diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts
index f23ef63ce5..1129bb551a 100644
--- a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts
+++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts
@@ -1,4 +1,5 @@
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 { Observable } from 'rxjs/Observable';
@@ -69,25 +70,26 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
constructor(
elementRef: ElementRef,
- private embedComponentsService: EmbedComponentsService,
- private logger: Logger,
- private titleService: Title,
- private metaService: Meta,
+ private embedComponentsService: EmbedComponentsService,
+ private logger: Logger,
+ private titleService: Title,
+ private metaService: Meta,
private tocService: TocService
) {
this.hostElement = elementRef.nativeElement;
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
this.hostElement.innerHTML = initialDocViewerContent;
+ swapOriginAndResult(this.hostElement);
- if ( this.hostElement.firstElementChild){
+ if (this.hostElement.firstElementChild) {
this.currViewContainer = this.hostElement.firstElementChild as HTMLElement;
}
this.onDestroy$.subscribe(() => this.destroyEmbeddedComponents());
this.docContents$
- .switchMap(newDoc => this.render(newDoc))
- .takeUntil(this.onDestroy$)
- .subscribe();
+ .switchMap(newDoc => this.render(newDoc))
+ .takeUntil(this.onDestroy$)
+ .subscribe();
}
ngDoCheck() {
@@ -116,11 +118,11 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
if (hasToc) {
titleEl!.insertAdjacentHTML('afterend', '');
- }
+ }
return () => {
this.tocService.reset();
- let title: string|null = '';
+ let title: string | null = '';
// Only create ToC for docs with an `
` heading.
// If you don't want a ToC, add "no-toc" class to ``.
@@ -145,23 +147,24 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
this.setNoIndex(doc.id === FILE_NOT_FOUND_ID || doc.id === FETCHING_ERROR_ID);
return this.void$
- // Security: `doc.contents` is always authored by the documentation team
- // and is considered to be safe.
- .do(() => this.nextViewContainer.innerHTML = doc.contents || '')
- .do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id))
- .switchMap(() => this.embedComponentsService.embedInto(this.nextViewContainer))
- .do(() => this.docReady.emit())
- .do(() => this.destroyEmbeddedComponents())
- .do(componentRefs => this.embeddedComponentRefs = componentRefs)
- .switchMap(() => this.swapViews(addTitleAndToc))
- .do(() => this.docRendered.emit())
- .catch(err => {
- const errorMessage = (err instanceof Error) ? err.stack : err;
- this.logger.error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`);
- this.nextViewContainer.innerHTML = '';
- this.setNoIndex(true);
- return this.void$;
- });
+ // Security: `doc.contents` is always authored by the documentation team
+ // and is considered to be safe.
+ .do(() => this.nextViewContainer.innerHTML = doc.contents || '')
+ .do(() => swapOriginAndResult(this.nextViewContainer))
+ .do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id))
+ .switchMap(() => this.embedComponentsService.embedInto(this.nextViewContainer))
+ .do(() => this.docReady.emit())
+ .do(() => this.destroyEmbeddedComponents())
+ .do(componentRefs => this.embeddedComponentRefs = componentRefs)
+ .switchMap(() => this.swapViews(addTitleAndToc))
+ .do(() => this.docRendered.emit())
+ .catch(err => {
+ const errorMessage = (err instanceof Error) ? err.stack : err;
+ this.logger.error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`);
+ this.nextViewContainer.innerHTML = '';
+ this.setNoIndex(true);
+ return this.void$;
+ });
}
/**
@@ -169,8 +172,8 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
*/
private setNoIndex(val: boolean) {
if (val) {
- this.metaService.addTag({ name: 'googlebot', content: 'noindex' });
- this.metaService.addTag({ name: 'robots', content: 'noindex' });
+ this.metaService.addTag({name: 'googlebot', content: 'noindex'});
+ this.metaService.addTag({name: 'robots', content: 'noindex'});
} else {
this.metaService.removeTag('name="googlebot"');
this.metaService.removeTag('name="robots"');
@@ -204,26 +207,26 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
return 1000 * seconds;
};
const animateProp =
- (elem: HTMLElement, prop: keyof CSSStyleDeclaration, from: string, to: string, duration = 200) => {
- const animationsDisabled = !DocViewerComponent.animationsEnabled
- || this.hostElement.classList.contains(NO_ANIMATIONS);
- if (prop === 'length' || prop === 'parentRule') {
- // We cannot animate length or parentRule properties because they are readonly
- return this.void$;
- }
- elem.style.transition = '';
- return animationsDisabled
- ? this.void$.do(() => elem.style[prop] = to)
- : this.void$
- // 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
- // `transition` style, we need to ensure an animation frame has passed between
- // setting each style.
- .switchMap(() => raf$).do(() => elem.style[prop] = from)
- .switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`)
- .switchMap(() => raf$).do(() => (elem.style as any)[prop] = to)
- .switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$);
- };
+ (elem: HTMLElement, prop: keyof CSSStyleDeclaration, from: string, to: string, duration = 200) => {
+ const animationsDisabled = !DocViewerComponent.animationsEnabled
+ || this.hostElement.classList.contains(NO_ANIMATIONS);
+ if (prop === 'length' || prop === 'parentRule') {
+ // We cannot animate length or parentRule properties because they are readonly
+ return this.void$;
+ }
+ elem.style.transition = '';
+ return animationsDisabled
+ ? this.void$.do(() => elem.style[prop] = to)
+ : this.void$
+ // 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
+ // `transition` style, we need to ensure an animation frame has passed between
+ // setting each style.
+ .switchMap(() => raf$).do(() => elem.style[prop] = from)
+ .switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`)
+ .switchMap(() => raf$).do(() => (elem.style as any)[prop] = to)
+ .switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$);
+ };
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.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) {
done$ = done$
- // Remove the current view from the viewer.
- .switchMap(() => animateLeave(this.currViewContainer))
- .do(() => this.currViewContainer.parentElement!.removeChild(this.currViewContainer))
- .do(() => this.docRemoved.emit());
+ // Remove the current view from the viewer.
+ .switchMap(() => animateLeave(this.currViewContainer))
+ .do(() => this.currViewContainer.parentElement!.removeChild(this.currViewContainer))
+ .do(() => this.docRemoved.emit());
}
return done$
- // Insert the next view into the viewer.
- .do(() => this.hostElement.appendChild(this.nextViewContainer))
- .do(() => onInsertedCb())
- .do(() => this.docInserted.emit())
- .switchMap(() => animateEnter(this.nextViewContainer))
- // Update the view references and clean up unused nodes.
- .do(() => {
- const prevViewContainer = this.currViewContainer;
- this.currViewContainer = this.nextViewContainer;
- this.nextViewContainer = prevViewContainer;
- this.nextViewContainer.innerHTML = ''; // Empty to release memory.
- });
+ // Insert the next view into the viewer.
+ .do(() => this.hostElement.appendChild(this.nextViewContainer))
+ .do(() => onInsertedCb())
+ .do(() => this.docInserted.emit())
+ .switchMap(() => animateEnter(this.nextViewContainer))
+ // Update the view references and clean up unused nodes.
+ .do(() => {
+ const prevViewContainer = this.currViewContainer;
+ this.currViewContainer = this.nextViewContainer;
+ this.nextViewContainer = prevViewContainer;
+ 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);
+ }
}
}