import { DOCUMENT } from '@angular/common' ; import { HttpClient , HttpErrorResponse } from '@angular/common/http' ; import { ErrorHandler , Inject , Injectable , OnDestroy , Optional , SecurityContext , } from '@angular/core' ; import { DomSanitizer , SafeHtml , SafeResourceUrl } from '@angular/platform-browser' ; import { forkJoin , Observable , of as observableOf , throwError as observableThrow } from 'rxjs' ; import { catchError , finalize , map , share , tap } from 'rxjs/operators' ; docs-private export function getSbbIconNameNotFoundError ( iconName ) { return Error ( `Unable to find icon with the name "${ iconName }"` ) ; } docs-private export function getSbbIconNoHttpProviderError ( ) { return Error ( 'Could not find HttpClient provider for use with Sbb Angular icons. ' + 'Please include the HttpClientModule from @angular/common/http in your ' + 'app imports.' ) ; } param url docs-private export function getSbbIconFailedToSanitizeUrlError ( url ) { return Error ( `The URL provided to SbbIconRegistry was not trusted as a resource URL ` + `via Angular's DomSanitizer. Attempted URL was "${ url }".` ) ; } param literal docs-private export function getSbbIconFailedToSanitizeLiteralError ( literal ) { return Error ( `The literal provided to SbbIconRegistry was not trusted as safe HTML by ` + `Angular's DomSanitizer. Attempted literal was "${ literal }".` ) ; } /** Options that can be used to configure how an icon or the icons in an icon set are presented. */ export interface SbbIconOptions { /** View box to set on the icon. */ viewBox ? : string ; /** Whether or not to fetch the icon or icon set using HTTP credentials. */ withCredentials ? : boolean ; } /**
 * Function that will be invoked by the icon registry when trying to resolve the
 * URL from which to fetch an icon. The returned URL will be used to make a request for the icon.
 */ export type IconResolver = ( name : string , namespace : string ) => SafeResourceUrl | SafeResourceUrlWithIconOptions | null ; /** Object that specifies a URL from which to fetch an icon and the options to use for it. */ export interface SafeResourceUrlWithIconOptions { url : SafeResourceUrl ; options : SbbIconOptions ; } docs-private class SvgIconConfig { svgElement : SVGElement | null ; constructor ( public url , public svgText , public options ? ) { } } /** Icon configuration whose content has already been loaded. */ type LoadedSvgIconConfig = SvgIconConfig & { svgText : string ; } ; /**
 * Service to register and display icons used by the `<sbb-icon>` component.
 * - Registers icon URLs by namespace and name.
 * - Registers icon set URLs by namespace.
 * - Registers aliases for CSS classes, for use with icon fonts.
 * - Loads icons from URLs and extracts individual icons from icon sets.
 */ @ Injectable ( { providedIn : 'root' } ) export class SbbIconRegistry implements OnDestroy { private _document : Document ; /** URLs and cached SVG elements for individual icons. Keys are of the format "[namespace]:[icon]". */ private _svgIconConfigs = new Map < string , SvgIconConfig > ( ) ; /**
     * SvgIconConfig objects and cached SVG elements for icon sets, keyed by namespace.
     * Multiple icon sets can be registered under the same namespace.
     */ private _iconSetConfigs = new Map < string , SvgIconConfig [ ] > ( ) ; /** Cache for icons loaded by direct URLs. */ private _cachedIconsByUrl = new Map < string , SVGElement > ( ) ; /** In-progress icon fetches. Used to coalesce multiple requests to the same URL. */ private _inProgressUrlFetches = new Map < string , Observable < string > > ( ) ; /** Map from font identifiers to their CSS class names. Used for icon fonts. */ private _fontCssClassesByAlias = new Map < string , string > ( ) ; /** Registered icon resolver functions. */ private _resolvers : IconResolver [ ] = [ ] ; /**
     * The CSS class to apply when an `<sbb-icon>` component has no icon name, url, or font specified.
     * The default is 'sbb-icons'.
     */ private _defaultFontSetClass = 'sbb-icons' ; /**
     * A list of internally supported namespaces, which can be served from the icon CDN.
     * (See https://icons.app.sbb.ch/)
     */ private _sbbCdnSupportedNamespaces = [ 'kom' , 'fpl' ] ; constructor ( @ Optional ( ) private _httpClient , private _sanitizer , @ Optional ( ) @ Inject ( DOCUMENT ) document , private readonly _errorHandler ) { this . _document = document ; } param iconName addSvgIcon ( iconName , url , options ? ) { return this . addSvgIconInNamespace ( '' , iconName , url , options ) ; } param iconName param literal addSvgIconLiteral ( iconName , literal , options ? ) { return this . addSvgIconLiteralInNamespace ( '' , iconName , literal , options ) ; } param namespace param iconName addSvgIconInNamespace ( namespace , iconName , url , options ? ) { return this . _addSvgIconConfig ( namespace , iconName , new SvgIconConfig ( url , null , options ) ) ; } param resolver addSvgIconResolver ( resolver ) { this . _resolvers . push ( resolver ) ; return this ; } param namespace param iconName param literal addSvgIconLiteralInNamespace ( namespace , iconName , literal , options ? ) { const cleanLiteral = this . _sanitizer . sanitize ( SecurityContext . HTML , literal ) ; if ( ! cleanLiteral ) { throw getSbbIconFailedToSanitizeLiteralError ( literal ) ; } return this . _addSvgIconConfig ( namespace , iconName , new SvgIconConfig ( '' , cleanLiteral , options ) ) ; } /**
     * Registers an icon set by URL in the default namespace.
     */ addSvgIconSet ( url , options ? ) { return this . addSvgIconSetInNamespace ( '' , url , options ) ; } param literal addSvgIconSetLiteral ( literal , options ? ) { return this . addSvgIconSetLiteralInNamespace ( '' , literal , options ) ; } param namespace addSvgIconSetInNamespace ( namespace , url , options ? ) { return this . _addSvgIconSetConfig ( namespace , new SvgIconConfig ( url , null , options ) ) ; } param namespace param literal addSvgIconSetLiteralInNamespace ( namespace , literal , options ? ) { const cleanLiteral = this . _sanitizer . sanitize ( SecurityContext . HTML , literal ) ; if ( ! cleanLiteral ) { throw getSbbIconFailedToSanitizeLiteralError ( literal ) ; } return this . _addSvgIconSetConfig ( namespace , new SvgIconConfig ( '' , cleanLiteral , options ) ) ; } param alias param className registerFontClassAlias ( alias , className = alias ) { this . _fontCssClassesByAlias . set ( alias , className ) ; return this ; } /**
     * Returns the CSS class name associated with the alias by a previous call to
     * registerFontClassAlias. If no CSS class has been associated, returns the alias unmodified.
     */ classNameForFontAlias ( alias ) { return this . _fontCssClassesByAlias . get ( alias ) || alias ; } /**
     * Sets the CSS class name to be used for icon fonts when an `<sbb-icon>` component does not
     * have a fontSet input value, and is not loading an icon by name or URL.
     */ setDefaultFontSetClass ( className ) { this . _defaultFontSetClass = className ; return this ; } /**
     * Returns the CSS class name to be used for icon fonts when an `<sbb-icon>` component does not
     * have a fontSet input value, and is not loading an icon by name or URL.
     */ getDefaultFontSetClass ( ) { return this . _defaultFontSetClass ; } param safeUrl getSvgIconFromUrl ( safeUrl ) { const url = this . _sanitizer . sanitize ( SecurityContext . RESOURCE_URL , safeUrl ) ; if ( ! url ) { throw getSbbIconFailedToSanitizeUrlError ( safeUrl ) ; } const cachedIcon = this . _cachedIconsByUrl . get ( url ) ; if ( cachedIcon ) { return observableOf ( cloneSvg ( cachedIcon ) ) ; } return this . _loadSvgIconFromConfig ( new SvgIconConfig ( safeUrl , null ) ) . pipe ( tap ( ( svg ) => this . _cachedIconsByUrl . set ( url ! , svg ) ) , map ( ( svg ) => cloneSvg ( svg ) ) ) ; } param name param namespace getNamedSvgIcon ( name , namespace = '' ) { const key = iconKey ( namespace , name ) ; let config = this . _svgIconConfigs . get ( key ) ; if ( config ) { return this . _getSvgFromConfig ( config ) ; } config = this . _getIconConfigFromResolvers ( namespace , name ) ; if ( config ) { this . _svgIconConfigs . set ( key , config ) ; return this . _getSvgFromConfig ( config ) ; } const iconSetConfigs = this . _iconSetConfigs . get ( namespace ) ; if ( iconSetConfigs ) { return this . _getSvgFromIconSetConfigs ( name , iconSetConfigs ) ; } return observableThrow ( getSbbIconNameNotFoundError ( key ) ) ; } ngOnDestroy ( ) { this . _resolvers = [ ] ; this . _svgIconConfigs . clear ( ) ; this . _iconSetConfigs . clear ( ) ; this . _cachedIconsByUrl . clear ( ) ; } /**
     * Returns the cached icon for a SvgIconConfig if available, or fetches it from its URL if not.
     */ private _getSvgFromConfig ( config ) { if ( config . svgText ) { return observableOf ( cloneSvg ( this . _svgElementFromConfig ( config as LoadedSvgIconConfig ) ) ) ; } else { return this . _loadSvgIconFromConfig ( config ) . pipe ( map ( ( svg ) => cloneSvg ( svg ) ) ) ; } } /**
     * Attempts to find an icon with the specified name in any of the SVG icon sets.
     * First searches the available cached icons for a nested element with a matching name, and
     * if found copies the element to a new `<svg>` element. If not found, fetches all icon sets
     * that have not been cached, and searches again after all fetches are completed.
     * The returned Observable produces the SVG element if possible, and throws
     * an error if no icon with the specified name can be found.
     */ private _getSvgFromIconSetConfigs ( name , iconSetConfigs ) { const namedIcon = this . _extractIconWithNameFromAnySet ( name , iconSetConfigs ) ; if ( namedIcon ) { return observableOf ( namedIcon ) ; } const iconSetFetchRequests = iconSetConfigs . filter ( ( iconSetConfig ) => ! iconSetConfig . svgText ) . map ( ( iconSetConfig ) => { return this . _loadSvgIconSetFromConfig ( iconSetConfig ) . pipe ( catchError ( ( err ) => { const url = this . _sanitizer . sanitize ( SecurityContext . RESOURCE_URL , iconSetConfig . url ) ; const errorMessage = `Loading icon set URL: ${ url } failed: ${ err . message }` ; this . _errorHandler . handleError ( new Error ( errorMessage ) ) ; return observableOf ( null ) ; } ) ) ; } ) ; return forkJoin ( iconSetFetchRequests ) . pipe ( map ( ( ) => { const foundIcon = this . _extractIconWithNameFromAnySet ( name , iconSetConfigs ) ; if ( ! foundIcon ) { throw getSbbIconNameNotFoundError ( name ) ; } return foundIcon ; } ) ) ; } /**
     * Searches the cached SVG elements for the given icon sets for a nested icon element whose "id"
     * tag matches the specified name. If found, copies the nested element to a new SVG element and
     * returns it. Returns null if no matching element is found.
     */ private _extractIconWithNameFromAnySet ( iconName , iconSetConfigs ) { for ( let i = iconSetConfigs . length - 1 ; i >= 0 ; i -- ) { const config = iconSetConfigs [ i ] ; if ( config . svgText && config . svgText . indexOf ( iconName ) > - 1 ) { const svg = this . _svgElementFromConfig ( config as LoadedSvgIconConfig ) ; const foundIcon = this . _extractSvgIconFromSet ( svg , iconName , config . options ) ; if ( foundIcon ) { return foundIcon ; } } } return null ; } /**
     * Loads the content of the icon URL specified in the SvgIconConfig and creates an SVG element
     * from it.
     */ private _loadSvgIconFromConfig ( config ) { return this . _fetchIcon ( config ) . pipe ( tap ( ( svgText ) => ( config . svgText = svgText ) ) , map ( ( ) => this . _svgElementFromConfig ( config as LoadedSvgIconConfig ) ) ) ; } /**
     * Loads the content of the icon set URL specified in the
     * SvgIconConfig and attaches it to the config.
     */ private _loadSvgIconSetFromConfig ( config ) { if ( config . svgText ) { return observableOf ( null ) ; } return this . _fetchIcon ( config ) . pipe ( tap ( ( svgText ) => ( config . svgText = svgText ) ) ) ; } /**
     * Searches the cached element of the given SvgIconConfig for a nested icon element whose "id"
     * tag matches the specified name. If found, copies the nested element to a new SVG element and
     * returns it. Returns null if no matching element is found.
     */ private _extractSvgIconFromSet ( iconSet , iconName , options ? ) { const iconSource = iconSet . querySelector ( `[id="${ iconName }"]` ) ; if ( ! iconSource ) { return null ; } const iconElement = iconSource . cloneNode ( true ) as Element ; iconElement . removeAttribute ( 'id' ) ; if ( iconElement . nodeName . toLowerCase ( ) === 'svg' ) { return this . _setSvgAttributes ( iconElement as SVGElement , options ) ; } if ( iconElement . nodeName . toLowerCase ( ) === 'symbol' ) { return this . _setSvgAttributes ( this . _toSvgElement ( iconElement ) , options ) ; } const svg = this . _svgElementFromString ( '<svg></svg>' ) ; svg . appendChild ( iconElement ) ; return this . _setSvgAttributes ( svg , options ) ; } /** Creates a DOM element from the given SVG string. */ private _svgElementFromString ( str ) { const div = this . _document . createElement ( 'DIV' ) ; div . innerHTML = str ; const svg = div . querySelector ( 'svg' ) as SVGElement ; if ( ! svg ) { throw Error ( '<svg> tag not found' ) ; } return svg ; } /** Converts an element into an SVG node by cloning all of its children. */ private _toSvgElement ( element ) { const svg = this . _svgElementFromString ( '<svg></svg>' ) ; const attributes = element . attributes ; for ( let i = 0 ; i < attributes . length ; i ++ ) { const { name , value } = attributes [ i ] ; if ( name !== 'id' ) { svg . setAttribute ( name , value ) ; } } for ( let i = 0 ; i < element . childNodes . length ; i ++ ) { if ( element . childNodes [ i ] . nodeType === this . _document . ELEMENT_NODE ) { svg . appendChild ( element . childNodes [ i ] . cloneNode ( true ) ) ; } } return svg ; } /** Sets the default attributes for an SVG element to be used as an icon. */ private _setSvgAttributes ( svg , options ? ) { svg . setAttribute ( 'fit' , '' ) ; svg . setAttribute ( 'preserveAspectRatio' , 'xMidYMid meet' ) ; svg . setAttribute ( 'focusable' , 'false' ) ; if ( options && options . viewBox ) { svg . setAttribute ( 'viewBox' , options . viewBox ) ; } return svg ; } /**
     * Returns an Observable which produces the string contents of the given icon. Results may be
     * cached, so future calls with the same URL may not cause another HTTP request.
     */ private _fetchIcon ( iconConfig ) { const { url : safeUrl , options } = iconConfig ; const withCredentials = options ?. withCredentials ?? false ; if ( ! this . _httpClient ) { throw getSbbIconNoHttpProviderError ( ) ; } if ( safeUrl == null ) { throw Error ( `Cannot fetch icon from URL "${ safeUrl }".` ) ; } const url = this . _sanitizer . sanitize ( SecurityContext . RESOURCE_URL , safeUrl ) ; if ( ! url ) { throw getSbbIconFailedToSanitizeUrlError ( safeUrl ) ; } const inProgressFetch = this . _inProgressUrlFetches . get ( url ) ; if ( inProgressFetch ) { return inProgressFetch ; } const req = this . _httpClient . get ( url , { responseType : 'text' , withCredentials } ) . pipe ( finalize ( ( ) => this . _inProgressUrlFetches . delete ( url ) ) , share ( ) ) ; this . _inProgressUrlFetches . set ( url , req ) ; return req ; } param namespace param iconName param config private _addSvgIconConfig ( namespace , iconName , config ) { this . _svgIconConfigs . set ( iconKey ( namespace , iconName ) , config ) ; return this ; } param namespace param config private _addSvgIconSetConfig ( namespace , config ) { const configNamespace = this . _iconSetConfigs . get ( namespace ) ; if ( configNamespace ) { configNamespace . push ( config ) ; } else { this . _iconSetConfigs . set ( namespace , [ config ] ) ; } return this ; } /** Parses a config's text into an SVG element. */ private _svgElementFromConfig ( config ) { if ( ! config . svgElement ) { const svg = this . _svgElementFromString ( config . svgText ) ; this . _setSvgAttributes ( svg , config . options ) ; config . svgElement = svg ; } return config . svgElement ; } /** Tries to create an icon config through the registered resolver functions. */ private _getIconConfigFromResolvers ( namespace , name ) { for ( let i = 0 ; i < this . _resolvers . length ; i ++ ) { const result = this . _resolvers [ i ] ( name , namespace ) ; if ( result ) { return isSafeUrlWithOptions ( result ) ? new SvgIconConfig ( result . url , null , result . options ) : new SvgIconConfig ( result , null ) ; } } if ( this . _sbbCdnSupportedNamespaces . indexOf ( namespace ) !== - 1 ) { const url = this . _sanitizer . bypassSecurityTrustResourceUrl ( `https://icons.app.sbb.ch/${ namespace }/${ name }.svg` ) ; return new SvgIconConfig ( url , null ) ; } return undefined ; } } /** Clones an SVGElement while preserving type information. */ function cloneSvg ( svg ) { return svg . cloneNode ( true ) as SVGElement ; } /** Returns the cache key to use for an icon namespace and name. */ function iconKey ( namespace , name ) { return namespace + ':' + name ; } function isSafeUrlWithOptions ( value ) { return ! ! ( value . url && value . options ) ; }