license import { Inject , Injectable , InjectionToken , OnDestroy , Optional , SecurityContext , SkipSelf } from '@angular/core' ; import { DOCUMENT } from '@angular/common' ; import { HttpClient , HttpErrorResponse } from '@angular/common/http' ; import { DomSanitizer , SafeResourceUrl , SafeHtml } from '@angular/platform-browser' ; import { forkJoin , Observable , of as observableOf , throwError as observableThrow } from 'rxjs' ; import { catchError , finalize , map , share , tap } from 'rxjs/operators' ; /**
 * Returns an exception to be thrown in the case when attempting to
 * load an icon with a name that cannot be found.
 */ export function getMdcIconNameNotFoundError ( iconName ) { return Error ( `Unable to find icon with the name "${ iconName }"` ) ; } docs-private export function getMdcIconNoHttpProviderError ( ) { return Error ( 'Could not find HttpClient provider for use with Angular Material icons. ' + 'Please include the HttpClientModule from @angular/common/http in your ' + 'app imports.' ) ; } param url docs-private export function getMdcIconFailedToSanitizeUrlError ( url ) { return Error ( `The URL provided to MatIconRegistry was not trusted as a resource URL ` + `via Angular's DomSanitizer. Attempted URL was "${ url }".` ) ; } param literal docs-private export function getMdcIconFailedToSanitizeLiteralError ( literal ) { return Error ( `The literal provided to MatIconRegistry 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 IconOptions { /** View box to set on the icon. */ viewBox ? : string ; } docs-private class SvgIconConfig { url : SafeResourceUrl | null = null ; svgElement : SVGElement | null = null ; constructor ( url , options ? ) ; constructor ( svgElement , options ? ) ; constructor ( data , public options ? ) { if ( ! ! ( data as any ) . nodeName ) { this . svgElement = data as SVGElement ; } else { this . url = data as SafeResourceUrl ; } } } /**
 * Service to register and display icons used by the `<mdc-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 MdcIconRegistry 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 > ( ) ; /**
     * The CSS classes to apply when an `<mdc-icon>` component has no icon name, url, or font specified.
     * The default 'material-icons' value assumes that the material icon font has been loaded as
     * described at http://google.github.io/material-design-icons/#icon-font-for-the-web
     */ private _defaultFontSetClass = [ 'material-icons' ] ; constructor ( @ Optional ( ) private _httpClient , private _sanitizer , @ Optional ( ) @ Inject ( DOCUMENT ) document ) { this . _document = document ; } param iconName param url 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 param url addSvgIconInNamespace ( namespace , iconName , url , options ? ) { return this . _addSvgIconConfig ( namespace , iconName , new SvgIconConfig ( url , options ) ) ; } param namespace param iconName param literal addSvgIconLiteralInNamespace ( namespace , iconName , literal , options ? ) { const sanitizedLiteral = this . _sanitizer . sanitize ( SecurityContext . HTML , literal ) ; if ( ! sanitizedLiteral ) { throw getMdcIconFailedToSanitizeLiteralError ( literal ) ; } const svgElement = this . _createSvgElementForSingleIcon ( sanitizedLiteral , options ) ; return this . _addSvgIconConfig ( namespace , iconName , new SvgIconConfig ( svgElement , options ) ) ; } param url addSvgIconSet ( url , options ? ) { return this . addSvgIconSetInNamespace ( '' , url , options ) ; } param literal addSvgIconSetLiteral ( literal , options ? ) { return this . addSvgIconSetLiteralInNamespace ( '' , literal , options ) ; } param namespace param url addSvgIconSetInNamespace ( namespace , url , options ? ) { return this . _addSvgIconSetConfig ( namespace , new SvgIconConfig ( url , options ) ) ; } param namespace param literal addSvgIconSetLiteralInNamespace ( namespace , literal , options ? ) { const sanitizedLiteral = this . _sanitizer . sanitize ( SecurityContext . HTML , literal ) ; if ( ! sanitizedLiteral ) { throw getMdcIconFailedToSanitizeLiteralError ( literal ) ; } const svgElement = this . _svgElementFromString ( sanitizedLiteral ) ; return this . _addSvgIconSetConfig ( namespace , new SvgIconConfig ( svgElement , 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 ; } param className setDefaultFontSetClass ( ... classNames ) { this . _defaultFontSetClass = classNames ; return this ; } /**
     * Returns the CSS class name to be used for icon fonts when an `<mdc-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 getMdcIconFailedToSanitizeUrlError ( safeUrl ) ; } const cachedIcon = this . _cachedIconsByUrl . get ( url ) ; if ( cachedIcon ) { return observableOf ( cloneSvg ( cachedIcon ) ) ; } return this . _loadSvgIconFromConfig ( new SvgIconConfig ( safeUrl ) ) . pipe ( tap ( svg => this . _cachedIconsByUrl . set ( url ! , svg ) ) , map ( svg => cloneSvg ( svg ) ) ) ; } param name param namespace getNamedSvgIcon ( name , namespace = '' ) { const key = iconKey ( namespace , name ) ; const config = this . _svgIconConfigs . get ( key ) ; if ( config ) { return this . _getSvgFromConfig ( config ) ; } const iconSetConfigs = this . _iconSetConfigs . get ( namespace ) ; if ( iconSetConfigs ) { return this . _getSvgFromIconSetConfigs ( name , iconSetConfigs ) ; } return observableThrow ( getMdcIconNameNotFoundError ( key ) ) ; } ngOnDestroy ( ) { 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 . svgElement ) { return observableOf ( cloneSvg ( config . svgElement ) ) ; } else { return this . _loadSvgIconFromConfig ( config ) . pipe ( tap ( svg => config . svgElement = svg ) , 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 . 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 observableOf ( null ) ; } ) ) ; } ) ; return forkJoin ( iconSetFetchRequests ) . pipe ( map ( ( ) => { const foundIcon = this . _extractIconWithNameFromAnySet ( name , iconSetConfigs ) ; if ( ! foundIcon ) { throw getMdcIconNameNotFoundError ( 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 . svgElement ) { const foundIcon = this . _extractSvgIconFromSet ( config . svgElement , 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 . _fetchUrl ( config . url ) . pipe ( map ( svgText => this . _createSvgElementForSingleIcon ( svgText , config . options ) ) ) ; } /**
     * Loads the content of the icon set URL specified in the SvgIconConfig and creates an SVG element
     * from it.
     */ private _loadSvgIconSetFromConfig ( config ) { if ( config . svgElement ) { return observableOf ( config . svgElement ) ; } return this . _fetchUrl ( config . url ) . pipe ( map ( svgText => { if ( ! config . svgElement ) { config . svgElement = this . _svgElementFromString ( svgText ) ; } return config . svgElement ; } ) ) ; } /**
     * Creates a DOM element from the given SVG string, and adds default attributes.
     */ private _createSvgElementForSingleIcon ( responseText , options ? ) { const svg = this . _svgElementFromString ( responseText ) ; this . _setSvgAttributes ( svg , options ) ; return svg ; } /**
     * 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 ( 'height' , '100%' ) ; svg . setAttribute ( 'width' , '100%' ) ; 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 URL. Results may be
     * cached, so future calls with the same URL may not cause another HTTP request.
     */ private _fetchUrl ( safeUrl ) { if ( ! this . _httpClient ) { throw getMdcIconNoHttpProviderError ( ) ; } if ( safeUrl == null ) { throw Error ( `Cannot fetch icon from URL "${ safeUrl }".` ) ; } const url = this . _sanitizer . sanitize ( SecurityContext . RESOURCE_URL , safeUrl ) ; if ( ! url ) { throw getMdcIconFailedToSanitizeUrlError ( safeUrl ) ; } const inProgressFetch = this . _inProgressUrlFetches . get ( url ) ; if ( inProgressFetch ) { return inProgressFetch ; } const req = this . _httpClient . get ( url , { responseType : 'text' } ) . 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 ; } } docs-private export function ICON_REGISTRY_PROVIDER_FACTORY ( parentRegistry , httpClient , sanitizer , document ? ) { return parentRegistry || new MdcIconRegistry ( httpClient , sanitizer , document ) ; } docs-private export const ICON_REGISTRY_PROVIDER = { provide : MdcIconRegistry , deps : [ [ new Optional ( ) , new SkipSelf ( ) , MdcIconRegistry ] , [ new Optional ( ) , HttpClient ] , DomSanitizer , [ new Optional ( ) , DOCUMENT as InjectionToken < any > ] , ] , useFactory : ICON_REGISTRY_PROVIDER_FACTORY , } ; /** 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 ; }