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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM UTC coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

79.25
/projects/igniteui-angular/src/lib/icon/icon.service.ts
1
import { DestroyRef, 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 = {
35✔
54
        name: "material",
55
        meta: { className: "material-icons", type: "liga" },
56
    };
57
    private _iconRefs = new Map<string, Map<string, IconMeta>>();
35✔
58
    private _families = new Map<string, FamilyMeta>();
35✔
59
    private _cachedIcons = new Map<string, Map<string, SafeHtml>>();
35✔
60
    private _iconLoaded = new Subject<IgxIconLoadedEvent>();
35✔
61
    private _domParser: DOMParser;
62

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

72
        this.iconLoaded = this._iconLoaded.asObservable();
35✔
73
        this.setFamily(this._defaultFamily.name, this._defaultFamily.meta);
35✔
74

75
        const { unsubscribe } = this._themeToken?.onChange((theme) => {
35✔
76
            this.setRefsByTheme(theme);
35✔
77
        });
78

79
        this._destroyRef.onDestroy(() => unsubscribe);
35✔
80

81
        if (this._platformUtil?.isBrowser) {
35✔
82
            this._domParser = new DOMParser();
35✔
83

84
            for (const [name, svg] of IndigoIcons) {
35✔
85
                this.addSvgIconFromText(name, svg.value, `internal_${svg.fontSet}`, true);
1,155✔
86
            }
87
        }
88
    }
89

90
    /**
91
     *  Returns the default font-family.
92
     * ```typescript
93
     *   const defaultFamily = this.iconService.defaultFamily;
94
     * ```
95
     */
96
    public get defaultFamily(): IconFamily {
97
        return this._defaultFamily;
1,210✔
98
    }
99

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

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

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

137
    /** @hidden @internal */
138
    private familyType(alias: string): IconType {
139
        return this._families.get(alias)?.type;
4,882✔
140
    }
141

142
    /** @hidden @internal */
143
    public setRefsByTheme(theme: IgxTheme) {
144
        for (const { alias, target } of iconReferences) {
35✔
145
            const external = this._iconRefs.get(alias.family)?.get(alias.name)?.external;
3,115✔
146

147
            const _ref = this._iconRefs.get('default')?.get(alias.name) ?? {};
3,115✔
148
            const _target = target.get(theme) ?? target.get('default')!;
3,115✔
149

150
            const icon = target.get(theme) ?? target.get('default')!;
3,115✔
151
            const overwrite = !external && !(JSON.stringify(_ref) === JSON.stringify(_target));
3,115✔
152

153
            this._setIconRef(
3,115✔
154
                alias.name,
155
                alias.family,
156
                icon,
157
                overwrite
158
            );
159
        }
160
    }
161

162
    /**
163
     *  Creates a family to className relationship that is applied to the IgxIconComponent
164
     *   whenever that family name is used.
165
     * ```typescript
166
     *   this.iconService.setFamily('material', { className: 'material-icons', type: 'liga' });
167
     * ```
168
     */
169
    public setFamily(name: string, meta: FamilyMeta) {
170
        this._families.set(name, meta);
35✔
171
    }
172

173
    /**
174
     *  Adds an icon reference meta for an icon in a meta family.
175
     *  Executes only if no icon reference is found.
176
     * ```typescript
177
     *   this.iconService.addIconRef('aruba', 'default', { name: 'aruba', family: 'svg-flags' });
178
     * ```
179
     */
180
    public addIconRef(name: string, family: string, icon: IconMeta) {
181
        const iconRef = this._iconRefs.get(family)?.get(name);
2,680✔
182

183
        if (!iconRef) {
2,680✔
184
            this.setIconRef(name, family, icon);
1,767✔
185
        }
186
    }
187

188
    private _setIconRef(name: string, family: string, icon: IconMeta, overwrite = false) {
×
189
        if (overwrite) {
3,115✔
190
            this.setIconRef(name, family, {
3,115✔
191
                ...icon,
192
                external: false
193
            });
194
        }
195
    }
196

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

206
        if (!familyRef) {
4,882✔
207
            familyRef = new Map<string, IconMeta>();
35✔
208
            this._iconRefs.set(family, familyRef);
35✔
209
        }
210

211
        const external = icon.external ?? true;
4,882✔
212
        const familyType = this.familyType(icon?.family);
4,882✔
213
        familyRef.set(name, { ...icon, type: icon.type ?? familyType, external });
4,882✔
214

215
        this._iconLoaded.next({ name, family });
4,882✔
216
    }
217

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

227
        const iconFamily = icon?.family ?? family;
2,513!
228
        const _name = icon?.name ?? name;
2,513!
229
        const className = this.familyClassName(iconFamily);
2,513✔
230
        const prefix = this._families.get(iconFamily)?.prefix;
2,513✔
231

232
        // Handle name prefixes
233
        let iconName = _name;
2,513✔
234

235
        if (iconName && prefix) {
2,513!
UNCOV
236
            iconName = _name.includes(prefix) ? _name : `${prefix}${_name}`;
×
237
        }
238

239
        const cached = this.isSvgIconCached(iconName, iconFamily);
2,513✔
240
        const type = cached ? "svg" : icon?.type ?? this.familyType(iconFamily);
2,513!
241

242
        return {
2,513✔
243
            className,
244
            type,
245
            name: iconName,
246
            family: iconFamily,
247
        };
248
    }
249

250
    private getOrCreateSvgFamily(family: string) {
251
        if (!this._families.has(family)) {
3,282✔
252
            this._families.set(family, { className: family, type: "svg" });
70✔
253
        }
254

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

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

UNCOV
278
            const sanitizedUrl = this._sanitizer.sanitize(
×
279
                SecurityContext.RESOURCE_URL,
280
                safeUrl,
281
            );
282

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

UNCOV
289
            if (!this.isSvgIconCached(name, family)) {
×
UNCOV
290
                this.getOrCreateSvgFamily(family);
×
291

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

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

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

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

342
            return familyRegistry.has(name);
11,275✔
343
        }
344

345
        return false;
2,057✔
346
    }
347

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

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

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

379
            if (!this._cachedIcons.has(family)) {
3,282✔
380
                this._cachedIcons.set(family, new Map<string, SafeHtml>());
70✔
381
            }
382

383
            if (svg) {
3,282✔
384
                svg.setAttribute("fit", "");
3,282✔
385
                svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
3,282✔
386

387
                if (stripMeta) {
3,282✔
388
                    const title = svg.querySelector("title");
1,155✔
389
                    const desc = svg.querySelector("desc");
1,155✔
390

391
                    if (title) {
1,155!
UNCOV
392
                        svg.removeChild(title);
×
393
                    }
394

395
                    if (desc) {
1,155!
UNCOV
396
                        svg.removeChild(desc);
×
397
                    }
398
                }
399

400
                const safeSvg = this._sanitizer.bypassSecurityTrustHtml(
3,282✔
401
                    svg.outerHTML,
402
                );
403

404
                this._cachedIcons.get(family).set(name, safeSvg);
3,282✔
405
                this._iconLoaded.next({ name, value, family });
3,282✔
406
            }
407
        }
408
    }
409
}
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