2017-07-31 15:45:18 +03:00
|
|
|
import { Component, ComponentRef, DoCheck, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
|
|
|
import { Title } from '@angular/platform-browser';
|
|
|
|
|
|
|
|
import { Observable } from 'rxjs/Observable';
|
|
|
|
import { of } from 'rxjs/observable/of';
|
|
|
|
import 'rxjs/add/operator/catch';
|
|
|
|
import 'rxjs/add/operator/do';
|
|
|
|
import 'rxjs/add/operator/switchMap';
|
|
|
|
import 'rxjs/add/operator/takeUntil';
|
2017-03-01 14:30:25 +00:00
|
|
|
|
|
|
|
import { DocumentContents } from 'app/documents/document.service';
|
2017-07-31 15:45:18 +03:00
|
|
|
import { EmbedComponentsService } from 'app/embed-components/embed-components.service';
|
|
|
|
import { Logger } from 'app/shared/logger.service';
|
2017-04-27 15:32:46 -07:00
|
|
|
import { TocService } from 'app/shared/toc.service';
|
2017-03-01 14:30:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
// Initialization prevents flicker once pre-rendering is on
|
|
|
|
const initialDocViewerElement = document.querySelector('aio-doc-viewer');
|
|
|
|
const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : '';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'aio-doc-viewer',
|
2017-04-27 15:32:46 -07:00
|
|
|
template: ''
|
2017-03-01 14:30:25 +00:00
|
|
|
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
|
|
|
// encapsulation: ViewEncapsulation.Native
|
|
|
|
})
|
|
|
|
export class DocViewerComponent implements DoCheck, OnDestroy {
|
|
|
|
|
|
|
|
private hostElement: HTMLElement;
|
|
|
|
|
2017-07-31 15:45:18 +03:00
|
|
|
private void$ = of<void>(undefined);
|
|
|
|
private onDestroy$ = new EventEmitter<void>();
|
|
|
|
private docContents$ = new EventEmitter<DocumentContents>();
|
|
|
|
|
|
|
|
protected embeddedComponentRefs: ComponentRef<any>[] = [];
|
|
|
|
|
|
|
|
@Input()
|
|
|
|
set doc(newDoc: DocumentContents) {
|
|
|
|
// Ignore `undefined` values that could happen if the host component
|
|
|
|
// does not initially specify a value for the `doc` input.
|
|
|
|
if (newDoc) {
|
|
|
|
this.docContents$.emit(newDoc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-13 09:20:09 +00:00
|
|
|
@Output()
|
2017-07-31 15:45:18 +03:00
|
|
|
docRendered = new EventEmitter<void>();
|
2017-03-13 09:20:09 +00:00
|
|
|
|
2017-03-01 14:30:25 +00:00
|
|
|
constructor(
|
|
|
|
elementRef: ElementRef,
|
2017-07-31 15:45:18 +03:00
|
|
|
private embedComponentsService: EmbedComponentsService,
|
|
|
|
private logger: Logger,
|
2017-04-27 15:32:46 -07:00
|
|
|
private titleService: Title,
|
|
|
|
private tocService: TocService
|
2017-03-01 14:30:25 +00:00
|
|
|
) {
|
|
|
|
this.hostElement = elementRef.nativeElement;
|
|
|
|
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
|
|
|
this.hostElement.innerHTML = initialDocViewerContent;
|
|
|
|
|
2017-07-31 15:45:18 +03:00
|
|
|
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
|
|
|
|
this.docContents$
|
|
|
|
.do(() => this.destroyEmbeddedComponents())
|
|
|
|
.switchMap(newDoc => this.render(newDoc))
|
|
|
|
.do(() => this.docRendered.emit())
|
|
|
|
.takeUntil(this.onDestroy$)
|
|
|
|
.subscribe();
|
2017-03-01 14:30:25 +00:00
|
|
|
}
|
|
|
|
|
2017-07-31 15:45:18 +03:00
|
|
|
ngDoCheck() {
|
|
|
|
this.embeddedComponentRefs.forEach(comp => comp.changeDetectorRef.detectChanges());
|
2017-03-01 14:30:25 +00:00
|
|
|
}
|
|
|
|
|
2017-07-31 15:45:18 +03:00
|
|
|
ngOnDestroy() {
|
|
|
|
this.onDestroy$.emit();
|
2017-03-01 14:30:25 +00:00
|
|
|
}
|
|
|
|
|
2017-07-31 15:45:18 +03:00
|
|
|
/**
|
|
|
|
* Set up the window title and ToC.
|
|
|
|
*/
|
|
|
|
protected addTitleAndToc(docId: string): void {
|
2017-04-27 15:32:46 -07:00
|
|
|
this.tocService.reset();
|
|
|
|
const titleEl = this.hostElement.querySelector('h1');
|
2017-11-15 03:01:00 +02:00
|
|
|
let title = '';
|
|
|
|
|
2017-04-27 15:32:46 -07:00
|
|
|
// Only create TOC for docs with an <h1> title
|
2017-04-27 15:33:50 -07:00
|
|
|
// If you don't want a TOC, add "no-toc" class to <h1>
|
2017-04-27 15:32:46 -07:00
|
|
|
if (titleEl) {
|
2017-11-15 03:01:00 +02:00
|
|
|
title = (typeof titleEl.innerText === 'string') ? titleEl.innerText : titleEl.textContent;
|
2017-04-27 15:32:46 -07:00
|
|
|
if (!/(no-toc|notoc)/i.test(titleEl.className)) {
|
|
|
|
this.tocService.genToc(this.hostElement, docId);
|
|
|
|
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
|
|
|
|
}
|
|
|
|
}
|
2017-11-15 03:01:00 +02:00
|
|
|
|
2017-04-27 15:32:46 -07:00
|
|
|
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
|
|
|
|
}
|
|
|
|
|
2017-07-31 15:45:18 +03:00
|
|
|
/**
|
|
|
|
* Destroy the embedded components to avoid memory leaks.
|
|
|
|
*/
|
|
|
|
protected destroyEmbeddedComponents(): void {
|
|
|
|
this.embeddedComponentRefs.forEach(comp => comp.destroy());
|
|
|
|
this.embeddedComponentRefs = [];
|
2017-03-01 14:30:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-07-31 15:45:18 +03:00
|
|
|
* Add doc content to host element and build it out with embedded components.
|
2017-03-01 14:30:25 +00:00
|
|
|
*/
|
2017-07-31 15:45:18 +03:00
|
|
|
protected render(doc: DocumentContents): Observable<void> {
|
|
|
|
return this.void$
|
|
|
|
.do(() => {
|
|
|
|
// Security: `doc.contents` is always authored by the documentation team
|
|
|
|
// and is considered to be safe.
|
|
|
|
this.hostElement.innerHTML = doc.contents || '';
|
|
|
|
this.addTitleAndToc(doc.id);
|
|
|
|
})
|
|
|
|
.switchMap(() => this.embedComponentsService.embedInto(this.hostElement))
|
|
|
|
.do(componentRefs => this.embeddedComponentRefs = componentRefs)
|
|
|
|
.switchMap(() => this.void$)
|
|
|
|
.catch(err => {
|
|
|
|
this.logger.error(`[DocViewer]: Error preparing document '${doc.id}'.`, err);
|
|
|
|
return this.void$;
|
|
|
|
});
|
2017-03-01 14:30:25 +00:00
|
|
|
}
|
|
|
|
}
|