Skip to content

Commit

Permalink
feat: control if workbench part content is capable of being moved in …
Browse files Browse the repository at this point in the history
…the DOM

This commit adds the structural component `ContentAsOverlayComponent` which ensures that its content children are not reparented in the DOM when workbench parts are layouted. Technically, content is added to a top-level workbench DOM element and its content projected into that component's bounding box.

For instance, an iframe would reload once it is reparented in the DOM.

Closes #30

BREAKING CHANGE:

Removed content projection from `RemoteSiteComponent` and added it to workbench part level. If using a remote site, wrap entire part content in a `<wb-content-as-overlay>` element, which causes it to be added to a top-level workbench DOM element and projected into that component's bounding box.
Removed support to use `RemoteSiteComponent` as a routing component because must be a child of `<wb-content-as-overlay>` element
  • Loading branch information
danielwiehl committed Dec 11, 2018
1 parent f589764 commit 303d29a
Show file tree
Hide file tree
Showing 25 changed files with 302 additions and 174 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import '../workbench.constants';
@import '../workbench.mixins';
@import '../content-projection/mixins';

$diamond-height: 8;

Expand Down Expand Up @@ -93,10 +93,6 @@ $diamond-height: 8;

wb-router-outlet + ::ng-deep * {
flex: 1 1 100%;

&:not([prevent-pointer-events="true"]) {
@include allowPointerEvents;
}
}
}

Expand All @@ -110,3 +106,8 @@ $diamond-height: 8;
}
}
}

:host-context([content-projection="false"]) sci-viewport {
@include allowPointerEvents;
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, HostBinding, ViewChild } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style, transition, trigger } from '@angular/animations';
import { WorkbenchActivityPartService } from './workbench-activity-part.service';
import { WorkbenchLayoutService } from '../workbench-layout.service';
import { noop, Observable, Subject } from 'rxjs';
import { ACTIVITY_OUTLET_NAME, ROUTER_OUTLET_NAME } from '../workbench.constants';
import { Activity } from './activity';
import { ContentProjectionContext } from '../content-projection/content-projection-context.service';

@Component({
selector: 'wb-activity-part',
Expand All @@ -35,7 +36,8 @@ import { Activity } from './activity';
)
],
viewProviders: [
{provide: ROUTER_OUTLET_NAME, useValue: ACTIVITY_OUTLET_NAME}
{provide: ROUTER_OUTLET_NAME, useValue: ACTIVITY_OUTLET_NAME},
ContentProjectionContext,
]
})
export class ActivityPartComponent {
Expand All @@ -52,11 +54,17 @@ export class ActivityPartComponent {
@ViewChild('panel', {read: ElementRef})
private _panelElementRef: ElementRef;

@HostBinding('attr.content-projection')
public get contentProjectionActive(): boolean {
return this._contentProjectionContext.isActive();
}

constructor(public host: ElementRef<HTMLElement>,
public activityPartService: WorkbenchActivityPartService,
private _workbenchLayout: WorkbenchLayoutService,
private _animationBuilder: AnimationBuilder,
private _cd: ChangeDetectorRef) {
private _cd: ChangeDetectorRef,
private _contentProjectionContext: ContentProjectionContext) {
}

public get activities(): Activity[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- define area where to project `ng-content` into -->
<div class="projection-target"
wbTemplateHostOverlay
[wbTemplate]="ng_content_template"
[wbOverlayHost]="contentHost">
</div>

<!-- make `ng-content` available in the form of a template -->
<ng-template #ng_content_template>
<!-- wrap `ng-content` in a a single element -->
<div class="content-as-overlay">
<ng-content></ng-content>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
:host {
display: flex;

> div.projection-target {
flex: auto;
}
}

div.content-as-overlay {
display: flex; // public API
flex-direction: column; // public API
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { Component, OnDestroy, Optional, ViewContainerRef } from '@angular/core';
import { ContentHostRef } from './content-host-ref.service';
import { ContentProjectionContext } from './content-projection-context.service';
import { WorkbenchView } from '../workbench.model';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { WorkbenchActivityPartService } from '../activity-part/workbench-activity-part.service';
import { ActivatedRoute } from '@angular/router';


/**
* Structural component which adds its `ng-content` to a top-level workbench DOM element and projects it into this component's bounding box.
*
* This component ensures that its content children are not reparented in the DOM when workbench views are rearranged or activities toggled.
* For instance, an iframe would reload once it is reparented in the DOM.
*
* Use this component to wrap the entire content of your component, so `<wb-content-as-overlay>` is the only root view child of your component.
*
* `ng-content` is added to a flex-box container with `flex-direction` set to 'column'.
* To style elements of `ng-content`, do not combine CSS selectors with :host CSS pseudo-class because not a child of the host component.
*
*
* ---
* Example HTML template:
*
* <wb-content-as-overlay>
* <wb-remote-site [url]="..."></wb-remote-site>
* </wb-content-as-overlay>
*
*
* Example SCSS styles:
*
* :host {
* display: flex;
* > wb-content-as-overlay {
* flex: auto;
* }
* }
*
* wb-remote-site {
* flex: auto;
* }
*/
@Component({
selector: 'wb-content-as-overlay',
templateUrl: './content-as-overlay.component.html',
styleUrls: ['./content-as-overlay.component.scss']
})
export class ContentAsOverlayComponent implements OnDestroy {

private _destroy$ = new Subject<void>();
public contentHost: ViewContainerRef;

constructor(contentHostRef: ContentHostRef,
private _contentProjection: ContentProjectionContext,
@Optional() view: WorkbenchView,
activityPartService: WorkbenchActivityPartService,
route: ActivatedRoute) {
this.contentHost = contentHostRef.get();

this.installViewActiveListener(view);
this.installActivityActiveListener(activityPartService, route);
}

private installActivityActiveListener(activityService: WorkbenchActivityPartService, route: ActivatedRoute): void {
const activity = activityService.getActivityFromRoutingContext(route.snapshot);
activity && activity.active$
.pipe(takeUntil(this._destroy$))
.subscribe(active => this._contentProjection.setActive(active));
}

private installViewActiveListener(view: WorkbenchView): void {
view && view.active$
.pipe(takeUntil(this._destroy$))
.subscribe(active => this._contentProjection.setActive(active));
}

public ngOnDestroy(): void {
this._destroy$.next();
this._contentProjection.setActive(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { Injectable, ViewContainerRef } from '@angular/core';

/**
* Represents the location in the DOM where to append content of views or activities, whose DOM elements
* are not allowed to be moved within the DOM or detached/re-attached during their lifecycle. This would
* happen when workbench views are rearranged or activities toggled.
*
* Instead, content is added to a top-level workbench DOM element and its content projected into the container's bounding box.
* To not cover other parts of the workbench (e.g. sashes or view dropdown), they are placed upfront in the DOM.
*
* For instance, an iframe would reload once it is reparented in the DOM.
*/
@Injectable()
export class ContentHostRef {

private _vcr: ViewContainerRef;

public set(vcr: ViewContainerRef): void {
if (this._vcr) {
throw Error('`ViewContainerRef` already set');
}
this._vcr = vcr;
}

public get(): ViewContainerRef {
return this._vcr;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';

/**
* Indicates if content projection is used in the current view or activity context.
*
* A separate instance is used for every {ViewComponent} and {ActivityPartComponent}.
*/
@Injectable()
export class ContentProjectionContext {

private _active = false;

public setActive(active: boolean): void {
// Set active flag asynchronously to not run into a `ExpressionChangedAfterItHasBeenCheckedError`,
// e.g. if evaluated by a component which already was change detected.
setTimeout(() => this._active = active);
}

public isActive(): boolean {
return this._active;
}
}
30 changes: 30 additions & 0 deletions projects/scion/workbench/src/lib/content-projection/mixins.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

/**
* Pointer events are ignored in 'activity part' and 'viewpart-grid' to allow interaction with projected content.
* Projected content is out of the element flow and added to a top-level workbench DOM element. It is projected
* into respective view or activity bounding box.
*
* To not cover other parts of the workbench (e.g. sashes), it is placed upfront in the DOM, which requires projection
* areas to ignore pointer events and not to use a background color.
*
* Use this mixin to allow pointer events in selected areas like sashes, activity header or view tabbar.
*/
@mixin allowPointerEvents() {
pointer-events: auto;
}

/**
* See mixin 'allowPointerEvents'
*/
@mixin preventPointerEvents() {
pointer-events: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
*/

import { AfterViewInit, Directive, DoCheck, ElementRef, EmbeddedViewRef, EventEmitter, Input, KeyValueDiffer, KeyValueDiffers, OnDestroy, OnInit, Optional, Output, Renderer2, TemplateRef, ViewContainerRef } from '@angular/core';
import { WorkbenchView } from './workbench.model';
import { WorkbenchView } from '../workbench.model';
import { ActivatedRoute } from '@angular/router';
import { takeUntil } from 'rxjs/operators';
import { WorkbenchActivityPartService } from './activity-part/workbench-activity-part.service';
import { Subject } from 'rxjs';
import { WorkbenchActivityPartService } from '../activity-part/workbench-activity-part.service';
import { combineLatest, Subject } from 'rxjs';
import { WorkbenchLayoutService } from '../workbench-layout.service';

/**
* Instantiates an Embedded View based on the {@link TemplateRef `templateRef`}, appends it
Expand Down Expand Up @@ -54,13 +55,14 @@ export class TemplateHostOverlayDirective implements OnInit, AfterViewInit, DoCh
private _renderer: Renderer2,
@Optional() view: WorkbenchView,
route: ActivatedRoute,
activityPartService: WorkbenchActivityPartService) {
activityPartService: WorkbenchActivityPartService,
workbenchLayoutService: WorkbenchLayoutService) {
this._host = host.nativeElement as Element;
this._positionDiffer = differs.find({}).create();
this._whenViewRef = new Promise<EmbeddedViewRef<void>>(resolve => this._viewRefResolveFn = resolve); // tslint:disable-line:typedef

this.installViewActiveListener(view);
this.installActivityActiveListener(activityPartService, route);
this.installActivityActiveListener(activityPartService, route, workbenchLayoutService);
this.setViewRefStyle({position: 'fixed'});
}

Expand Down Expand Up @@ -110,11 +112,11 @@ export class TemplateHostOverlayDirective implements OnInit, AfterViewInit, DoCh
});
}

private installActivityActiveListener(activityService: WorkbenchActivityPartService, route: ActivatedRoute): void {
private installActivityActiveListener(activityService: WorkbenchActivityPartService, route: ActivatedRoute, workbenchLayoutService: WorkbenchLayoutService): void {
const activity = activityService.getActivityFromRoutingContext(route.snapshot);
activity && activity.active$
activity && combineLatest(activity.active$, workbenchLayoutService.maximized$)
.pipe(takeUntil(this._destroy$))
.subscribe(active => this.setViewRefStyle({display: active ? null : 'none'}));
.subscribe(([active, maximized]) => this.setViewRefStyle({display: active && !maximized ? null : 'none'}));
}

private installViewActiveListener(view: WorkbenchView): void {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
<main wbTemplateHostOverlay
[wbTemplate]="iframe_template"
[wbOverlayHost]="iframeHostRef.get()"
(wbTemplateViewRef)="onIframeViewRef($event)">
</main>

<ng-template #iframe_template>
<iframe [src]="siteUrl"
(load)="onSiteLoad($event)"
scrolling="no"
frameborder="0"
marginheight="0"
marginwidth="0">
</iframe>
</ng-template>
<iframe [src]="siteUrl"
(load)="onSiteLoad($event)"
scrolling="no"
frameborder="0"
marginheight="0"
marginwidth="0">
</iframe>
Loading

0 comments on commit 303d29a

Please sign in to comment.