import { ComponentPortal , DomPortalOutlet } from '@angular/cdk/portal' ; import { HttpClient , HttpErrorResponse } from '@angular/common/http' ; import { ApplicationRef , Component , ComponentFactoryResolver , ElementRef , EventEmitter , Injectable , Injector , Input , NgZone , OnDestroy , Output , SecurityContext , ViewContainerRef } from '@angular/core' ; import { DomSanitizer } from '@angular/platform-browser' ; import { Observable , Subscription } from 'rxjs' ; import { shareReplay , take , tap } from 'rxjs/operators' ; import { ExampleViewer } from '../example-viewer/example-viewer' ; @ Injectable ( { providedIn : 'root' } ) class DocFetcher { private _cache : Record < string , Observable < string > > = { } ; constructor ( private _http ) { } fetchDocument ( url ) { if ( this . _cache [ url ] ) { return this . _cache [ url ] ; } const stream = this . _http . get ( url , { responseType : 'text' } ) . pipe ( shareReplay ( 1 ) ) ; return stream . pipe ( tap ( ( ) => this . _cache [ url ] = stream ) ) ; } } @ Component ( { selector : 'doc-viewer' , template : 'Loading document...' } ) export class DocViewer implements OnDestroy { /** The URL of the document to display. */ @ Input ( ) set documentUrl ( url ) { this . fetchDocument ( url ) ; } @ Output ( ) contentRendered = new EventEmitter < void > ( ) ; @ Output ( ) contentRenderFailed = new EventEmitter < void > ( ) ; /** The document text. It should not be HTML encoded. */ textContent = '' ; private portalHosts : DomPortalOutlet [ ] = [ ] ; private documentFetchSubscription : Subscription ; constructor ( private _appRef , private _componentFactoryResolver , private _elementRef , private _injector , private _viewContainerRef , private _ngZone , private _domSanitizer , private _docFetcher ) { } ngOnDestroy ( ) { this . clearLiveExamples ( ) ; this . documentFetchSubscription ?. unsubscribe ( ) ; } /** Fetch a document by URL. */ private fetchDocument ( url ) { this . documentFetchSubscription ?. unsubscribe ( ) ; this . documentFetchSubscription = this . _docFetcher . fetchDocument ( url ) . subscribe ( ( document ) => this . updateDocument ( document ) , ( error ) => this . showError ( url , error ) ) ; } param rawDocument private updateDocument ( rawDocument ) { rawDocument = rawDocument . replace ( /href="#([^"]*)"/g , ( _m , fragmentUrl ) => { const absoluteUrl = `${ location . pathname }#${ fragmentUrl }` ; return `href="${ this . _domSanitizer . sanitize ( SecurityContext . URL , absoluteUrl ) }"` ; } ) ; this . _elementRef . nativeElement . innerHTML = rawDocument ; this . textContent = this . _elementRef . nativeElement . textContent ; this . loadComponents ( 'mosaic-docs-example' , ExampleViewer ) ; this . _ngZone . onStable . pipe ( take ( 1 ) ) . subscribe ( ( ) => this . contentRendered . next ( ) ) ; } /** Show an error that occurred when fetching a document. */ private showError ( url , error ) { console . error ( error ) ; this . _elementRef . nativeElement . innerText = `Failed to load document: ${ url }. Error: ${ error . statusText }` ; this . _ngZone . onStable . pipe ( take ( 1 ) ) . subscribe ( ( ) => this . contentRenderFailed . next ( ) ) ; } /** Instantiate a ExampleViewer for each example. */ private loadComponents ( componentName , componentClass ) { const exampleElements = this . _elementRef . nativeElement . querySelectorAll ( `[${ componentName }]` ) ; Array . prototype . slice . call ( exampleElements ) . forEach ( ( element ) => { const example = element . getAttribute ( componentName ) ; const portalHost = new DomPortalOutlet ( element , this . _componentFactoryResolver , this . _appRef , this . _injector ) ; const examplePortal = new ComponentPortal ( componentClass , this . _viewContainerRef ) ; const exampleViewer = portalHost . attach ( examplePortal ) ; ( exampleViewer . instance as ExampleViewer ) . example = example ; this . portalHosts . push ( portalHost ) ; } ) ; } private clearLiveExamples ( ) { this . portalHosts . forEach ( ( h ) => h . dispose ( ) ) ; this . portalHosts = [ ] ; } }