import { forkJoin , Observable , of , throwError } from 'rxjs' ; import { catchError , finalize , map , share , tap } from 'rxjs/operators' ; import { DOCUMENT } from '@angular/common' ; import { HttpClient , HttpErrorResponse } from '@angular/common/http' ; import { Inject , Injectable , SecurityContext } from '@angular/core' ; import { DomSanitizer , SafeHtml , SafeResourceUrl } from '@angular/platform-browser' ; class SvgIconConfig { url : SafeResourceUrl | null ; svgElement : SVGElement | null ; constructor ( data ) { if ( data && ! ! ( data as any ) . nodeName ) { this . svgElement = data as SVGElement ; } else { this . url = data as SafeResourceUrl ; } } } export type IconMode = 'font' | 'svg' ; @ Injectable ( { providedIn : 'root' } ) export class ThyIconRegistry { private defaultFontSetClass = 'wt-icon' ; private internalIconMode : IconMode = 'svg' ; private svgIconConfigs = new Map < string , SvgIconConfig > ( ) ; private svgIconSetConfigs = new Map < string , SvgIconConfig [ ] > ( ) ; private inProgressUrlFetches = new Map < string , Observable < string > > ( ) ; public get iconMode ( ) { return this . internalIconMode ; } constructor ( private sanitizer , private httpClient , @ Inject ( DOCUMENT ) private document ) { } private getIconNameNotFoundError ( iconName ) { return Error ( `Unable to find icon with the name "${ iconName }"` ) ; } private getIconFailedToSanitizeLiteralError ( literal ) { return Error ( `The literal provided to ThyIconRegistry was not trusted as safe HTML by ` + `Angular's DomSanitizer. Attempted literal was "${ literal }".` ) ; } private internalAddSvgIconSet ( namespace , config ) { const configNamespace = this . svgIconSetConfigs . get ( namespace ) ; if ( configNamespace ) { configNamespace . push ( config ) ; } else { this . svgIconSetConfigs . set ( namespace , [ config ] ) ; } return this ; } private cloneSvg ( svg ) { return svg . cloneNode ( true ) as SVGElement ; } private fetchUrl ( safeUrl ) { if ( safeUrl == null ) { throw Error ( `Cannot fetch icon from URL "${ safeUrl }".` ) ; } const url = this . sanitizer . sanitize ( SecurityContext . RESOURCE_URL , safeUrl ) ; if ( ! url ) { throw new Error ( `The URL provided to ThyIconRegistry was not trusted as a resource URL ` + `via Angular's DomSanitizer. Attempted URL was "${ url }".` ) ; } const inProgressFetch = this . inProgressUrlFetches . get ( url ) ; if ( inProgressFetch ) { return inProgressFetch ; } else { const req = this . httpClient . get ( url , { responseType : 'text' } ) . pipe ( finalize ( ( ) => this . inProgressUrlFetches . delete ( url ) ) , share ( ) ) ; this . inProgressUrlFetches . set ( url , req ) ; return req ; } } private toSvgElement ( element ) { const svg = this . svgElementFromString ( '<svg></svg>' ) ; 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 ; } private extractSvgIconFromIconSet ( iconSet , iconName ) { 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 ) ; } if ( iconElement . nodeName . toLowerCase ( ) === 'symbol' ) { return this . setSvgAttributes ( this . toSvgElement ( iconElement ) ) ; } const svg = this . svgElementFromString ( '<svg></svg>' ) ; svg . appendChild ( iconElement ) ; return this . setSvgAttributes ( svg ) ; } private extractIconWithNameFromIconSetConfigs ( iconName , iconSetConfigs ) { for ( let i = iconSetConfigs . length - 1 ; i >= 0 ; i -- ) { const config = iconSetConfigs [ i ] ; if ( config . svgElement ) { const foundIcon = this . extractSvgIconFromIconSet ( config . svgElement , iconName ) ; if ( foundIcon ) { return foundIcon ; } } } return null ; } 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 ; } private setSvgAttributes ( svg ) { svg . setAttribute ( 'fit' , '' ) ; svg . setAttribute ( 'height' , '1em' ) ; svg . setAttribute ( 'width' , '1em' ) ; svg . setAttribute ( 'preserveAspectRatio' , 'xMidYMid meet' ) ; svg . setAttribute ( 'focusable' , 'false' ) ; return svg ; } private createSvgElementForSingleIcon ( responseText ) { const svg = this . svgElementFromString ( responseText ) ; this . setSvgAttributes ( svg ) ; return svg ; } private loadSvgIconFromConfig ( config ) { return this . fetchUrl ( config . url ) . pipe ( map ( svgText => this . createSvgElementForSingleIcon ( svgText ) ) ) ; } private loadSvgIconSetFromConfig ( config ) { if ( config . svgElement ) { return of ( config . svgElement ) ; } return this . fetchUrl ( config . url ) . pipe ( map ( svgText => { if ( ! config . svgElement ) { config . svgElement = this . svgElementFromString ( svgText ) ; } return config . svgElement ; } ) ) ; } private getSvgFromConfig ( config ) { if ( config . svgElement ) { return of ( this . cloneSvg ( config . svgElement ) ) ; } else { return this . loadSvgIconFromConfig ( config ) . pipe ( tap ( svg => ( config . svgElement = svg ) ) , map ( svg => this . cloneSvg ( svg ) ) ) ; } } private getSvgFromIconSetConfigs ( name , iconSetConfigs ) { const namedIcon = this . extractIconWithNameFromIconSetConfigs ( name , iconSetConfigs ) ; if ( namedIcon ) { return of ( namedIcon ) ; } const iconSetFetchRequests = iconSetConfigs . filter ( iconSetConfig => ! iconSetConfig . svgElement ) . map ( iconSetConfig => { return this . loadSvgIconSetFromConfig ( iconSetConfig ) . pipe ( catchError ( ( err ) : Observable < SVGElement | null > => { const url = this . sanitizer . sanitize ( SecurityContext . RESOURCE_URL , iconSetConfig . url ) ; console . error ( `Loading icon set URL: ${ url } failed: ${ err . message }` ) ; return of ( null ) ; } ) ) ; } ) ; return forkJoin ( iconSetFetchRequests ) . pipe ( map ( ( ) => { const foundIcon = this . extractIconWithNameFromIconSetConfigs ( name , iconSetConfigs ) ; if ( ! foundIcon ) { throw this . getIconNameNotFoundError ( name ) ; } return foundIcon ; } ) ) ; } private internalAddSvgIconConfig ( namespace , iconName , config ) { this . svgIconConfigs . set ( this . buildIconKey ( namespace , iconName ) , config ) ; return this ; } public buildIconKey ( namespace , name ) { return namespace + ':' + name ; } public splitIconName ( iconName ) { if ( ! iconName ) { return [ '' , '' ] ; } const parts = iconName . split ( ':' ) ; switch ( parts . length ) { case 1 : return [ '' , parts [ 0 ] ] ; case 2 : return < [ string , string ] > parts ; default : throw Error ( `Invalid icon name: "${ iconName }"` ) ; } } public addSvgIconSetInNamespace ( namespace , url ) { return this . internalAddSvgIconSet ( namespace , new SvgIconConfig ( url ) ) ; } public addSvgIconSet ( url ) { return this . addSvgIconSetInNamespace ( '' , url ) ; } public addSvgIconSetLiteralInNamespace ( namespace , literal ) { const sanitizedLiteral = this . sanitizer . sanitize ( SecurityContext . HTML , literal ) ; if ( ! sanitizedLiteral ) { throw this . getIconFailedToSanitizeLiteralError ( literal ) ; } const svgElement = this . svgElementFromString ( sanitizedLiteral ) ; return this . internalAddSvgIconSet ( namespace , new SvgIconConfig ( svgElement ) ) ; } public addSvgIconSetLiteral ( literal ) { return this . addSvgIconSetLiteralInNamespace ( '' , literal ) ; } param namespace param iconName param url public addSvgIconInNamespace ( namespace , iconName , url ) { return this . internalAddSvgIconConfig ( namespace , iconName , new SvgIconConfig ( url ) ) ; } param iconName param url public addSvgIcon ( iconName , url ) { return this . addSvgIconInNamespace ( '' , iconName , url ) ; } param iconName param literal public addSvgIconLiteral ( iconName , literal ) { return this . addSvgIconLiteralInNamespace ( '' , iconName , literal ) ; } param namespace param iconName param literal public addSvgIconLiteralInNamespace ( namespace , iconName , literal ) { const sanitizedLiteral = this . sanitizer . sanitize ( SecurityContext . HTML , literal ) ; if ( ! sanitizedLiteral ) { throw this . getIconFailedToSanitizeLiteralError ( literal ) ; } const svgElement = this . createSvgElementForSingleIcon ( sanitizedLiteral ) ; return this . internalAddSvgIconConfig ( namespace , iconName , new SvgIconConfig ( svgElement ) ) ; } public getDefaultFontSetClass ( ) { return this . defaultFontSetClass ; } public getFontSetClassByAlias ( fontSet ) { return fontSet ; } public getSvgIcon ( name , namespace = '' ) { const key = this . buildIconKey ( namespace , name ) ; const config = this . svgIconConfigs . get ( key ) ; if ( config ) { return this . getSvgFromConfig ( config ) ; } const iconSetConfigs = this . svgIconSetConfigs . get ( namespace ) ; if ( iconSetConfigs ) { return this . getSvgFromIconSetConfigs ( name , iconSetConfigs ) ; } return throwError ( this . getIconNameNotFoundError ( key ) ) ; } public setIconMode ( mode ) { this . internalIconMode = mode ; } }