fix: 重新合并 doc-viewer 中的翻译代码(未包含标题翻译)

This commit is contained in:
Zhicheng Wang 2018-03-07 17:16:30 +08:00
parent 0f8d7fd8ce
commit c29aee6729
1 changed files with 104 additions and 65 deletions

View File

@ -1,4 +1,5 @@
import { Component, ComponentRef, DoCheck, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; import { Component, ComponentRef, DoCheck, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { HostListener } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser'; import { Title, Meta } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
@ -69,25 +70,26 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
constructor( constructor(
elementRef: ElementRef, elementRef: ElementRef,
private embedComponentsService: EmbedComponentsService, private embedComponentsService: EmbedComponentsService,
private logger: Logger, private logger: Logger,
private titleService: Title, private titleService: Title,
private metaService: Meta, private metaService: Meta,
private tocService: TocService private tocService: TocService
) { ) {
this.hostElement = elementRef.nativeElement; this.hostElement = elementRef.nativeElement;
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure // Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
this.hostElement.innerHTML = initialDocViewerContent; this.hostElement.innerHTML = initialDocViewerContent;
swapOriginAndResult(this.hostElement);
if ( this.hostElement.firstElementChild){ if (this.hostElement.firstElementChild) {
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement; this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
} }
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents()); this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
this.docContents$ this.docContents$
.switchMap(newDoc => this.render(newDoc)) .switchMap(newDoc => this.render(newDoc))
.takeUntil(this.onDestroy$) .takeUntil(this.onDestroy$)
.subscribe(); .subscribe();
} }
ngDoCheck() { ngDoCheck() {
@ -116,11 +118,11 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
if (hasToc) { if (hasToc) {
titleEl!.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>'); titleEl!.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
} }
return () => { return () => {
this.tocService.reset(); this.tocService.reset();
let title: string|null = ''; let title: string | null = '';
// Only create ToC for docs with an `<h1>` heading. // Only create ToC for docs with an `<h1>` heading.
// If you don't want a ToC, add "no-toc" class to `<h1>`. // If you don't want a ToC, add "no-toc" class to `<h1>`.
@ -145,23 +147,24 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
this.setNoIndex(doc.id === FILE_NOT_FOUND_ID || doc.id === FETCHING_ERROR_ID); this.setNoIndex(doc.id === FILE_NOT_FOUND_ID || doc.id === FETCHING_ERROR_ID);
return this.void$ return this.void$
// Security: `doc.contents` is always authored by the documentation team // Security: `doc.contents` is always authored by the documentation team
// and is considered to be safe. // and is considered to be safe.
.do(() => this.nextViewContainer.innerHTML = doc.contents || '') .do(() => this.nextViewContainer.innerHTML = doc.contents || '')
.do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id)) .do(() => swapOriginAndResult(this.nextViewContainer))
.switchMap(() => this.embedComponentsService.embedInto(this.nextViewContainer)) .do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id))
.do(() => this.docReady.emit()) .switchMap(() => this.embedComponentsService.embedInto(this.nextViewContainer))
.do(() => this.destroyEmbeddedComponents()) .do(() => this.docReady.emit())
.do(componentRefs => this.embeddedComponentRefs = componentRefs) .do(() => this.destroyEmbeddedComponents())
.switchMap(() => this.swapViews(addTitleAndToc)) .do(componentRefs => this.embeddedComponentRefs = componentRefs)
.do(() => this.docRendered.emit()) .switchMap(() => this.swapViews(addTitleAndToc))
.catch(err => { .do(() => this.docRendered.emit())
const errorMessage = (err instanceof Error) ? err.stack : err; .catch(err => {
this.logger.error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`); const errorMessage = (err instanceof Error) ? err.stack : err;
this.nextViewContainer.innerHTML = ''; this.logger.error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`);
this.setNoIndex(true); this.nextViewContainer.innerHTML = '';
return this.void$; this.setNoIndex(true);
}); return this.void$;
});
} }
/** /**
@ -169,8 +172,8 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
*/ */
private setNoIndex(val: boolean) { private setNoIndex(val: boolean) {
if (val) { if (val) {
this.metaService.addTag({ name: 'googlebot', content: 'noindex' }); this.metaService.addTag({name: 'googlebot', content: 'noindex'});
this.metaService.addTag({ name: 'robots', content: 'noindex' }); this.metaService.addTag({name: 'robots', content: 'noindex'});
} else { } else {
this.metaService.removeTag('name="googlebot"'); this.metaService.removeTag('name="googlebot"');
this.metaService.removeTag('name="robots"'); this.metaService.removeTag('name="robots"');
@ -204,26 +207,26 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
return 1000 * seconds; return 1000 * seconds;
}; };
const animateProp = const animateProp =
(elem: HTMLElement, prop: keyof CSSStyleDeclaration, from: string, to: string, duration = 200) => { (elem: HTMLElement, prop: keyof CSSStyleDeclaration, from: string, to: string, duration = 200) => {
const animationsDisabled = !DocViewerComponent.animationsEnabled const animationsDisabled = !DocViewerComponent.animationsEnabled
|| this.hostElement.classList.contains(NO_ANIMATIONS); || this.hostElement.classList.contains(NO_ANIMATIONS);
if (prop === 'length' || prop === 'parentRule') { if (prop === 'length' || prop === 'parentRule') {
// We cannot animate length or parentRule properties because they are readonly // We cannot animate length or parentRule properties because they are readonly
return this.void$; return this.void$;
} }
elem.style.transition = ''; elem.style.transition = '';
return animationsDisabled return animationsDisabled
? this.void$.do(() => elem.style[prop] = to) ? this.void$.do(() => elem.style[prop] = to)
: this.void$ : this.void$
// In order to ensure that the `from` value will be applied immediately (i.e. // In order to ensure that the `from` value will be applied immediately (i.e.
// without transition) and that the `to` value will be affected by the // without transition) and that the `to` value will be affected by the
// `transition` style, we need to ensure an animation frame has passed between // `transition` style, we need to ensure an animation frame has passed between
// setting each style. // setting each style.
.switchMap(() => raf$).do(() => elem.style[prop] = from) .switchMap(() => raf$).do(() => elem.style[prop] = from)
.switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`) .switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`)
.switchMap(() => raf$).do(() => (elem.style as any)[prop] = to) .switchMap(() => raf$).do(() => (elem.style as any)[prop] = to)
.switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$); .switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$);
}; };
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1'); const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1');
const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.1', '1'); const animateEnter = (elem: HTMLElement) => animateProp(elem, 'opacity', '0.1', '1');
@ -232,24 +235,60 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
if (this.currViewContainer.parentElement) { if (this.currViewContainer.parentElement) {
done$ = done$ done$ = done$
// Remove the current view from the viewer. // Remove the current view from the viewer.
.switchMap(() => animateLeave(this.currViewContainer)) .switchMap(() => animateLeave(this.currViewContainer))
.do(() => this.currViewContainer.parentElement!.removeChild(this.currViewContainer)) .do(() => this.currViewContainer.parentElement!.removeChild(this.currViewContainer))
.do(() => this.docRemoved.emit()); .do(() => this.docRemoved.emit());
} }
return done$ return done$
// Insert the next view into the viewer. // Insert the next view into the viewer.
.do(() => this.hostElement.appendChild(this.nextViewContainer)) .do(() => this.hostElement.appendChild(this.nextViewContainer))
.do(() => onInsertedCb()) .do(() => onInsertedCb())
.do(() => this.docInserted.emit()) .do(() => this.docInserted.emit())
.switchMap(() => animateEnter(this.nextViewContainer)) .switchMap(() => animateEnter(this.nextViewContainer))
// Update the view references and clean up unused nodes. // Update the view references and clean up unused nodes.
.do(() => { .do(() => {
const prevViewContainer = this.currViewContainer; const prevViewContainer = this.currViewContainer;
this.currViewContainer = this.nextViewContainer; this.currViewContainer = this.nextViewContainer;
this.nextViewContainer = prevViewContainer; this.nextViewContainer = prevViewContainer;
this.nextViewContainer.innerHTML = ''; // Empty to release memory. this.nextViewContainer.innerHTML = ''; // Empty to release memory.
}); });
}
@HostListener('click', ['$event'])
toggleTranslationOrigin($event: MouseEvent): void {
const element = findTranslationResult($event.target as Element);
if (element && element.hasAttribute('translation-result')) {
const origin = element.nextElementSibling;
if (!origin || origin.hasAttribute('translation-result') || origin.tagName !== element.tagName) {
return;
}
if (origin.getAttribute('translation-origin') === 'on') {
origin.setAttribute('translation-origin', 'off');
} else {
origin.setAttribute('translation-origin', 'on');
}
}
}
}
function findTranslationResult(element: Element | null): Element | null {
while (element && !element.hasAttribute('translation-result')) {
element = element.parentElement;
}
return element;
}
function swapOriginAndResult(root: Element): void {
const results = root.querySelectorAll('[translation-result]');
for (let i = 0; i < results.length; ++i) {
const result = results.item(i);
const origin = result.previousElementSibling;
if (origin && origin.hasAttribute('translation-origin') && origin.parentElement) {
origin.parentElement.insertBefore(result, origin);
}
} }
} }