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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

14.29
/src/icon/icon-registry.ts
1
import { forkJoin, Observable, of, throwError } from 'rxjs';
2
import { catchError, finalize, map, share, tap } from 'rxjs/operators';
3

4
import { DOCUMENT } from '@angular/common';
5
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
6
import { Inject, Injectable, SecurityContext } from '@angular/core';
7
import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser';
8
import { isString } from 'ngx-tethys/util';
9

10
class SvgIconConfig {
11
    url: SafeResourceUrl | null;
12
    svgElement: SVGElement | null;
13

×
14
    constructor(data: SafeResourceUrl | SVGElement) {
×
15
        // Note that we can't use `instanceof SVGElement` here,
16
        // because it'll break during server-side rendering.
17
        if (data && !!(data as any).nodeName) {
×
18
            this.svgElement = data as SVGElement;
19
        } else {
20
            this.url = data as SafeResourceUrl;
21
        }
22
    }
23
}
24

1✔
25
export type IconMode = 'font' | 'svg';
26

2✔
27
export type SvgResourceUrl = SafeResourceUrl | string;
28

29
export type SvgHtml = SafeHtml | string;
1✔
30

1✔
31
/**
1✔
32
 * @order 20
1✔
33
 */
1✔
34
@Injectable({
1✔
35
    providedIn: 'root'
1✔
36
})
1✔
37
export class ThyIconRegistry {
38
    private defaultFontSetClass = 'wt-icon';
39
    private internalIconMode: IconMode = 'svg';
2✔
40
    private svgIconConfigs = new Map<string, SvgIconConfig>();
41
    private svgIconSetConfigs = new Map<string, SvgIconConfig[]>();
42
    private inProgressUrlFetches = new Map<string, Observable<string>>();
×
43

44
    public get iconMode() {
45
        return this.internalIconMode;
46
    }
×
47

×
48
    constructor(private sanitizer: DomSanitizer, private httpClient: HttpClient, @Inject(DOCUMENT) private document: any) {}
×
49

50
    private getIconNameNotFoundError(iconName: string): Error {
51
        return Error(`Unable to find icon with the name "${iconName}"`);
×
52
    }
53

×
54
    private getIconFailedToSanitizeLiteralError(literal: SvgHtml): Error {
55
        return Error(
56
            `The literal provided to ThyIconRegistry was not trusted as safe HTML by ` +
×
57
                `Angular's DomSanitizer. Attempted literal was "${literal}".`
58
        );
59
    }
×
60

×
61
    private internalAddSvgIconSet(namespace: string, config: SvgIconConfig): this {
62
        const configNamespace = this.svgIconSetConfigs.get(namespace);
×
63

×
64
        if (configNamespace) {
×
65
            configNamespace.push(config);
66
        } else {
67
            this.svgIconSetConfigs.set(namespace, [config]);
68
        }
69

70
        return this;
×
71
    }
×
72

×
73
    private cloneSvg(svg: SVGElement): SVGElement {
74
        return svg.cloneNode(true) as SVGElement;
75
    }
76

77
    private fetchUrl(safeUrl: SafeResourceUrl | null): Observable<string> {
×
78
        if (safeUrl == null) {
×
79
            throw Error(`Cannot fetch icon from URL "${safeUrl}".`);
×
80
        }
81

82
        const url = this.sanitizer.sanitize(SecurityContext.RESOURCE_URL, safeUrl);
83

×
84
        if ((typeof ngDevMode === 'undefined' || ngDevMode) && !url) {
×
85
            throw new Error(
×
86
                `The URL provided to ThyIconRegistry was not trusted as a resource URL ` +
×
87
                    `via Angular's DomSanitizer. Attempted URL was "${url}".`
88
            );
89
        }
×
90

91
        // Store in-progress fetches to avoid sending a duplicate request for a URL when there is
92
        // already a request in progress for that URL. It's necessary to call share() on the
93
        // Observable returned by http.get() so that multiple subscribers don't cause multiple XHRs.
94
        const inProgressFetch = this.inProgressUrlFetches.get(url);
×
95

×
96
        if (inProgressFetch) {
×
97
            return inProgressFetch;
98
        } else {
99
            // TODO(jelbourn): for some reason, the `finalize` operator "loses" the generic type on the
100
            // Observable. Figure out why and fix it.
×
101
            const req = this.httpClient.get(url, { responseType: 'text' }).pipe(
×
102
                finalize(() => this.inProgressUrlFetches.delete(url)),
103
                share()
104
            );
×
105

×
106
            this.inProgressUrlFetches.set(url, req);
107
            return req;
108
        }
109
    }
110

×
111
    private toSvgElement(element: Element): SVGElement {
×
112
        const svg = this.svgElementFromString('<svg></svg>');
113

114
        for (let i = 0; i < element.childNodes.length; i++) {
115
            if (element.childNodes[i].nodeType === this.document.ELEMENT_NODE) {
116
                svg.appendChild(element.childNodes[i].cloneNode(true));
117
            }
118
        }
×
119

120
        return svg;
×
121
    }
×
122

123
    private extractSvgIconFromIconSet(iconSet: SVGElement, iconName: string): SVGElement | null {
124
        // Use the `id="iconName"` syntax in order to escape special
125
        // characters in the ID (versus using the #iconName syntax).
×
126
        const iconSource = iconSet.querySelector(`[id="${iconName}"]`);
×
127

×
128
        if (!iconSource) {
×
129
            return null;
×
130
        }
×
131

132
        // Clone the element and remove the ID to prevent multiple elements from being added
133
        // to the page with the same ID.
134
        const iconElement = iconSource.cloneNode(true) as Element;
×
135
        iconElement.removeAttribute('id');
136

137
        // If the icon node is itself an <svg> node, clone and return it directly. If not, set it as
×
138
        // the content of a new <svg> node.
×
139
        if (iconElement.nodeName.toLowerCase() === 'svg') {
×
140
            return this.setSvgAttributes(iconElement as SVGElement);
×
141
        }
×
142

143
        // If the node is a <symbol>, it won't be rendered so we have to convert it into <svg>. Note
×
144
        // that the same could be achieved by referring to it via <use href="#id">, however the <use>
145
        // tag is problematic on Firefox, because it needs to include the current page path.
146
        if (iconElement.nodeName.toLowerCase() === 'symbol') {
×
147
            return this.setSvgAttributes(this.toSvgElement(iconElement));
×
148
        }
×
149

×
150
        // createElement('SVG') doesn't work as expected; the DOM ends up with
×
151
        // the correct nodes, but the SVG content doesn't render. Instead we
×
152
        // have to create an empty SVG node using innerHTML and append its content.
153
        // Elements created using DOMParser.parseFromString have the same problem.
154
        // http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display
×
155
        const svg = this.svgElementFromString('<svg></svg>');
×
156
        // Clone the node so we don't remove it from the parent icon set element.
×
157
        svg.appendChild(iconElement);
158

159
        return this.setSvgAttributes(svg);
×
160
    }
161

162
    private extractIconWithNameFromIconSetConfigs(iconName: string, iconSetConfigs: SvgIconConfig[]): SVGElement | null {
163
        // Iterate backwards, so icon sets added later have precedence.
×
164
        for (let i = iconSetConfigs.length - 1; i >= 0; i--) {
×
165
            const config = iconSetConfigs[i];
166
            if (config.svgElement) {
×
167
                const foundIcon = this.extractSvgIconFromIconSet(config.svgElement, iconName);
168
                if (foundIcon) {
169
                    return foundIcon;
×
170
                }
×
171
            }
172
        }
×
173
        return null;
174
    }
175

176
    private svgElementFromString(str: string): SVGElement {
×
177
        const div = this.document.createElement('DIV');
178
        div.innerHTML = str;
×
179
        const svg = div.querySelector('svg') as SVGElement;
180

181
        if (!svg) {
182
            throw Error('<svg> tag not found');
×
183
        }
184

185
        return svg;
186
    }
187

188
    private setSvgAttributes(svg: SVGElement): SVGElement {
×
189
        svg.setAttribute('fit', '');
×
190
        svg.setAttribute('height', '1em');
191
        svg.setAttribute('width', '1em');
192
        svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
193
        svg.setAttribute('focusable', 'false'); // Disable IE11 default behavior to make SVGs focusable.
×
194
        return svg;
195
    }
196

197
    private createSvgElementForSingleIcon(responseText: string): SVGElement {
×
198
        const svg = this.svgElementFromString(responseText);
×
199
        this.setSvgAttributes(svg);
200
        return svg;
×
201
    }
×
202

203
    private loadSvgIconFromConfig(config: SvgIconConfig): Observable<SVGElement> {
204
        return this.fetchUrl(config.url).pipe(map(svgText => this.createSvgElementForSingleIcon(svgText)));
×
205
    }
×
206

207
    private loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<SVGElement> {
208
        // If the SVG for this icon set has already been parsed, do nothing.
209
        if (config.svgElement) {
210
            return of(config.svgElement);
×
211
        }
×
212

×
213
        return this.fetchUrl(config.url).pipe(
×
214
            map(svgText => {
215
                // It is possible that the icon set was parsed and cached by an earlier request, so parsing
×
216
                // only needs to occur if the cache is yet unset.
217
                if (!config.svgElement) {
218
                    config.svgElement = this.svgElementFromString(svgText);
219
                }
×
220

×
221
                return config.svgElement;
222
            })
223
        );
2✔
224
    }
225

226
    private getSvgFromConfig(config: SvgIconConfig): Observable<SVGElement> {
2!
227
        if (config.svgElement) {
×
228
            // We already have the SVG element for this icon, return a copy.
229
            return of(this.cloneSvg(config.svgElement));
2✔
230
        } else {
2!
231
            // Fetch the icon from the config's URL, cache it, and return a copy.
232
            return this.loadSvgIconFromConfig(config).pipe(
2✔
233
                tap(svg => (config.svgElement = svg)),
234
                map(svg => this.cloneSvg(svg))
×
235
            );
236
        }
×
237
    }
238

239
    private getSvgFromIconSetConfigs(name: string, iconSetConfigs: SvgIconConfig[]): Observable<SVGElement> {
240
        // For all the icon set SVG elements we've fetched, see if any contain an icon with the
×
241
        // requested name.
×
242
        const namedIcon = this.extractIconWithNameFromIconSetConfigs(name, iconSetConfigs);
243

244
        if (namedIcon) {
245
            // We could cache namedIcon in svgIconConfigs, but since we have to make a copy every
246
            // time anyway, there's probably not much advantage compared to just always extracting
247
            // it from the icon set.
×
248
            return of(namedIcon);
249
        }
250

×
251
        // Not found in any cached icon sets. If there are icon sets with URLs that we haven't
×
252
        // fetched, fetch them now and look for iconName in the results.
×
253
        const iconSetFetchRequests: Observable<SVGElement | null>[] = iconSetConfigs
254
            .filter(iconSetConfig => !iconSetConfig.svgElement)
×
255
            .map(iconSetConfig => {
×
256
                return this.loadSvgIconSetFromConfig(iconSetConfig).pipe(
257
                    catchError((err: HttpErrorResponse): Observable<SVGElement | null> => {
258
                        const url = this.sanitizer.sanitize(SecurityContext.RESOURCE_URL, iconSetConfig.url);
×
259

260
                        // Swallow errors fetching individual URLs so the
261
                        // combined Observable won't necessarily fail.
262
                        console.error(`Loading icon set URL: ${url} failed: ${err.message}`);
263
                        return of(null);
264
                    })
265
                );
266
            });
267

268
        // Fetch all the icon set URLs. When the requests complete, every IconSet should have a
×
269
        // cached SVG element (unless the request failed), and we can check again for the icon.
×
270
        return forkJoin(iconSetFetchRequests).pipe(
271
            map(() => {
272
                const foundIcon = this.extractIconWithNameFromIconSetConfigs(name, iconSetConfigs);
273

274
                if (!foundIcon) {
275
                    throw this.getIconNameNotFoundError(name);
276
                }
277

278
                return foundIcon;
×
279
            })
280
        );
281
    }
282

283
    private internalAddSvgIconConfig(namespace: string, iconName: string, config: SvgIconConfig): this {
284
        this.svgIconConfigs.set(this.buildIconKey(namespace, iconName), config);
285
        return this;
286
    }
287

×
288
    public buildIconKey(namespace: string, name: string) {
289
        return namespace + ':' + name;
290
    }
291

292
    public splitIconName(iconName: string): [string, string] {
293
        if (!iconName) {
294
            return ['', ''];
295
        }
296
        const parts = iconName.split(':');
297
        switch (parts.length) {
×
298
            case 1:
×
299
                return ['', parts[0]]; // Use default namespace.
×
300
            case 2:
×
301
                return <[string, string]>parts;
302
            default:
×
303
                throw Error(`Invalid icon name: "${iconName}"`);
×
304
        }
305
    }
306

×
307
    public addSvgIconSetInNamespace(namespace: string, url: SvgResourceUrl): this {
308
        url = isString(url) ? this.sanitizer.bypassSecurityTrustResourceUrl(url) : url;
309
        return this.internalAddSvgIconSet(namespace, new SvgIconConfig(url));
×
310
    }
311

312
    /**
313
     * 添加SVG图标集,添加到默认命名空间
314
     */
×
315
    public addSvgIconSet(url: SvgResourceUrl): this {
316
        return this.addSvgIconSetInNamespace('', url);
2✔
317
    }
2✔
318

2!
319
    public addSvgIconSetLiteralInNamespace(namespace: string, literal: SafeHtml): this {
×
320
        const sanitizedLiteral = this.sanitizer.sanitize(SecurityContext.HTML, literal);
321

322
        if (!sanitizedLiteral) {
2✔
323
            throw this.getIconFailedToSanitizeLiteralError(literal);
2!
324
        }
×
325

326
        const svgElement = this.svgElementFromString(sanitizedLiteral);
2✔
327
        return this.internalAddSvgIconSet(namespace, new SvgIconConfig(svgElement));
328
    }
329

×
330
    public addSvgIconSetLiteral(literal: SafeHtml): this {
331
        return this.addSvgIconSetLiteralInNamespace('', literal);
1✔
332
    }
333

334
    /**
335
     * @description.en-us Registers an icon by URL in the specified namespace.
336
     * @description 添加单个SVG图标到指定的命名空间
337
     * @param namespace Namespace in which the icon should be registered.
1✔
338
     * @param iconName Name under which the icon should be registered.
339
     * @param url
340
     */
341
    public addSvgIconInNamespace(namespace: string, iconName: string, url: SvgResourceUrl): this {
342
        url = isString(url) ? this.sanitizer.bypassSecurityTrustResourceUrl(url) : url;
343
        return this.internalAddSvgIconConfig(namespace, iconName, new SvgIconConfig(url));
344
    }
345

346
    /**
347
     * @description.en-us Registers an icon by URL in the default namespace.
348
     * @description 添加单个SVG图标
349
     * @param iconName Name under which the icon should be registered.
350
     * @param url
351
     */
352
    public addSvgIcon(iconName: string, url: SvgResourceUrl): this {
353
        return this.addSvgIconInNamespace('', iconName, url);
354
    }
355

356
    /**
357
     * @description.en-us Registers an icon using an HTML string in the default namespace.
358
     * @description 添加单个SVG图标字符串,直接传入 SVG HTML 字符串
359
     * @param iconName Name under which the icon should be registered.
360
     * @param literal SVG source of the icon.
361
     */
362
    public addSvgIconLiteral(iconName: string, literal: SvgHtml): this {
363
        return this.addSvgIconLiteralInNamespace('', iconName, literal);
364
    }
365

366
    /**
367
     * @description.en-us Registers an icon using an HTML string in the specified namespace.
368
     * @description 添加单个SVG图标字符串到指定的命名空间,直接传入 SVG HTML 字符串
369
     * @param namespace Namespace in which the icon should be registered.
370
     * @param iconName Name under which the icon should be registered.
371
     * @param literal SVG source of the icon.
372
     */
373
    public addSvgIconLiteralInNamespace(namespace: string, iconName: string, literal: SvgHtml): this {
374
        literal = isString(literal) ? this.sanitizer.bypassSecurityTrustHtml(literal) : literal;
375
        const sanitizedLiteral = this.sanitizer.sanitize(SecurityContext.HTML, literal);
376

377
        if (!sanitizedLiteral) {
378
            throw this.getIconFailedToSanitizeLiteralError(literal);
379
        }
380

381
        const svgElement = this.createSvgElementForSingleIcon(sanitizedLiteral);
382
        return this.internalAddSvgIconConfig(namespace, iconName, new SvgIconConfig(svgElement));
383
    }
384

385
    public getDefaultFontSetClass() {
386
        return this.defaultFontSetClass;
387
    }
388

389
    public getFontSetClassByAlias(fontSet: string) {
390
        return fontSet;
391
    }
392

393
    /**
394
     * 获取某个图标
395
     */
396
    public getSvgIcon(name: string, namespace: string = ''): Observable<SVGElement> {
397
        // Return (copy of) cached icon if possible.
398
        const key = this.buildIconKey(namespace, name);
399
        const config = this.svgIconConfigs.get(key);
400

401
        if (config) {
402
            return this.getSvgFromConfig(config);
403
        }
404

405
        // See if we have any icon sets registered for the namespace.
406
        const iconSetConfigs = this.svgIconSetConfigs.get(namespace);
407

408
        if (iconSetConfigs) {
409
            return this.getSvgFromIconSetConfigs(name, iconSetConfigs);
410
        }
411

412
        return throwError(this.getIconNameNotFoundError(key));
413
    }
414

415
    public setIconMode(mode: IconMode) {
416
        this.internalIconMode = mode;
417
    }
418
}
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