All files / icon si-icons.ts

93.54% Statements 29/31
85.71% Branches 6/7
100% Functions 7/7
92.85% Lines 26/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92                            1x 2719x 2719x       2719x     1x                                             1x 1991x 1991x 1991x 3978x       3978x 3978x 3978x           1991x 1991x 3978x 3978x 2719x   1259x         1991x     12x     1x 902x     14x 14x       14x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { DestroyRef, inject, Injectable } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { SiThemeService } from '@siemens/element-ng/theme';
 
interface RegisteredIcon {
  content: SafeHtml | undefined;
  // Count how often an icon was registered to only remove it if it is no longer in use.
  referenceCount: number;
}
 
const parseDataSvgIcon = (icon: string, domSanitizer: DomSanitizer): SafeHtml => {
  const parsed = /^data:image\/svg\+xml;utf8,(.*)$/.exec(icon);
  Iif (!parsed) {
    console.error('Failed to parse icon', icon);
    return '';
  }
  return domSanitizer.bypassSecurityTrustHtml(parsed[1]);
};
 
const registeredIcons = new Map<string, RegisteredIcon>();
 
/**
 * Adds the provided icons.
 * It requires an Angular InjectionContent.
 * The Icons are available until the component is destroyed.
 * Call this function only in the component which actually uses the icon.
 * Importing all icons on the global level is discouraged.
 *
 * When using a string instead of the object to use an icon,
 * use the kebab-case version of the icon name.
 *
 * @example
 * ```ts
 * import { elementIcon } from '@simpl/element-icons/ionic';
 * import { addIcons } from '@siemens/element-ng/icon'
 *
 * @Component({`<si-icon [icon]="icons.elementIcon"`})
 * class MyComponent {
 *   icons = addIcons({ elementIcon })
 * }
 * ```
 */
export const addIcons = <T extends string>(icons: Record<T, string>): Record<T, string> => {
  const iconMap = {} as Record<T, string>;
  const domSanitizer = inject(DomSanitizer);
  for (const [key, rawContent] of Object.entries<string>(icons)) {
    const registeredIcon = registeredIcons.get(key) ?? {
      content: parseDataSvgIcon(rawContent, domSanitizer),
      referenceCount: 0
    };
    registeredIcon.referenceCount++;
    registeredIcons.set(key, registeredIcon);
    iconMap[key as T] = key;
  }
 
  // Delete registered Icons after Component is destroyed to optimize memory usage.
  // WeakMap must not be used, as the Icon can only be removed on component destruction.
  // When using a WeakMap it would also get destroyed if it is not referenced, but the component may use it later again.
  inject(DestroyRef).onDestroy(() => {
    for (const key of Object.keys(icons)) {
      const registeredIcon = registeredIcons.get(key);
      if (registeredIcon!.referenceCount === 1) {
        registeredIcons.delete(key);
      } else {
        registeredIcon!.referenceCount--;
      }
    }
  });
 
  return iconMap;
};
 
const getIcon = (key: string): SafeHtml | undefined => registeredIcons.get(key)?.content;
 
@Injectable({ providedIn: 'root' })
export class IconService {
  private themeService = inject(SiThemeService);
 
  getIcon(name: string): SafeHtml | undefined {
    const camelCaseName = this.kebabToCamelCase(name);
    return this.themeService.themeIcons()[camelCaseName] ?? getIcon(camelCaseName);
  }
 
  private kebabToCamelCase(str: string): string {
    return str?.replace(/-./g, match => match.charAt(1).toUpperCase());
  }
}