File

src/app/test-controller/components/unithost/unithost.component.ts

Implements

OnInit OnDestroy

Metadata

styleUrls ./unithost.component.css
templateUrl ./unithost.component.html

Index

Properties
Methods
HostListeners

Constructor

constructor(tcs: TestControllerService, mds: MainDataService, bs: BackendService, route: ActivatedRoute, snackBar: MatSnackBar)
Parameters :
Name Type Optional
tcs TestControllerService No
mds MainDataService No
bs BackendService No
route ActivatedRoute No
snackBar MatSnackBar No

HostListeners

window:resize
window:resize()

Methods

Private adjustIframeSize
adjustIframeSize()
Returns : void
Private Static getEnabledNavigationTargets
getEnabledNavigationTargets(nr: number, min: number, max: number, terminationAllowed: "ON" | "OFF" | "LAST_UNIT")
Parameters :
Name Type Optional Default value
nr number No
min number No
max number No
terminationAllowed "ON" | "OFF" | "LAST_UNIT" No 'ON'
Returns : VeronaNavigationTarget[]
Private getPlayerConfig
getPlayerConfig()
Returns : VeronaPlayerConfig
gotoPage
gotoPage(navigationTarget: string)
Parameters :
Name Type Optional
navigationTarget string No
Returns : void
Private handleIncomingMessage
handleIncomingMessage(messageEvent: MessageEvent)
Parameters :
Name Type Optional
messageEvent MessageEvent No
Returns : void
Private handleNavigationDenial
handleNavigationDenial(navigationDenial: literal type)
Parameters :
Name Type Optional
navigationDenial literal type No
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
onKeydownInClearCodeInput
onKeydownInClearCodeInput($event: KeyboardEvent)
Parameters :
Name Type Optional
$event KeyboardEvent No
Returns : void
onResize
onResize()
Decorators :
@HostListener('window:resize')
Returns : void
Private open
open(currentUnitSequenceId: number)
Parameters :
Name Type Optional
currentUnitSequenceId number No
Returns : void
Private prepareIframe
prepareIframe()
Returns : void
Private prepareUnit
prepareUnit()
Returns : void
Private runUnit
runUnit()
Returns : void
Private startTimerIfNecessary
startTimerIfNecessary()
Returns : void
verifyCodes
verifyCodes()
Returns : void

Properties

clearCodes
Type : object
Default value : {}
codeRequiringTestlets
Type : Testlet[]
Default value : []
currentPageIndex
Type : number
currentUnit
Type : UnitControllerData
currentUnitSequenceId
Default value : -1
Private iFrameHostElement
Type : HTMLElement
Private iFrameItemplayer
Type : HTMLIFrameElement
Private itemplayerSessionId
Type : string
Default value : ''
knownPages
Type : literal type[]
leaveWarning
Default value : false
Private pendingUnitData
Type : PendingUnitData
Default value : null
Private postMessageTarget
Type : Window
Default value : null
showPageNav
Default value : false
Private subscriptions
Type : literal type
Default value : {}
Public tcs
Type : TestControllerService
unitNavigationTarget
Default value : UnitNavigationTarget
unitsLoading$
Type : BehaviorSubject<LoadingProgress[]>
Default value : new BehaviorSubject<LoadingProgress[]>([])
unitsToLoadLabels
Type : string[]
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import {
  Component, HostListener, OnInit, OnDestroy
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
  TestStateKey,
  WindowFocusState,
  PendingUnitData,
  StateReportEntry,
  UnitStateKey,
  UnitPlayerState, LoadingProgress, UnitNavigationTarget
} from '../../interfaces/test-controller.interfaces';
import { BackendService } from '../../services/backend.service';
import { TestControllerService } from '../../services/test-controller.service';
import { MainDataService } from '../../../maindata.service';
import { VeronaNavigationDeniedReason, VeronaNavigationTarget, VeronaPlayerConfig } from '../../interfaces/verona.interfaces';
import { Testlet, UnitControllerData } from '../../classes/test-controller.classes';

declare let srcDoc;

@Component({
  templateUrl: './unithost.component.html',
  styleUrls: ['./unithost.component.css']
})

export class UnithostComponent implements OnInit, OnDestroy {
  private iFrameHostElement: HTMLElement;
  private iFrameItemplayer: HTMLIFrameElement;
  private subscriptions: { [tag: string ]: Subscription } = {};
  leaveWarning = false;

  showPageNav = false;

  currentUnitSequenceId = -1;

  private itemplayerSessionId = '';
  private postMessageTarget: Window = null;
  private pendingUnitData: PendingUnitData = null;

  knownPages: { id: string; label: string }[];
  unitsLoading$: BehaviorSubject<LoadingProgress[]> = new BehaviorSubject<LoadingProgress[]>([]);
  unitsToLoadLabels: string[];

  currentUnit: UnitControllerData;
  currentPageIndex: number;
  unitNavigationTarget = UnitNavigationTarget;

  clearCodes = {};
  codeRequiringTestlets: Testlet[] = [];

  constructor(
    public tcs: TestControllerService,
    private mds: MainDataService,
    private bs: BackendService,
    private route: ActivatedRoute,
    private snackBar: MatSnackBar
  ) { }

  ngOnInit(): void {
    this.iFrameHostElement = <HTMLElement>document.querySelector('#iframe-host');
    this.iFrameItemplayer = null;
    this.leaveWarning = false;
    setTimeout(() => {
      this.subscriptions.postMessage = this.mds.postMessage$
        .subscribe(messageEvent => this.handleIncomingMessage(messageEvent));
      this.subscriptions.routing = this.route.params
        .subscribe(params => this.open(Number(params.u)));
      this.subscriptions.navigationDenial = this.tcs.navigationDenial
        .subscribe(navigationDenial => this.handleNavigationDenial(navigationDenial));
    });
  }

  ngOnDestroy(): void {
    Object.values(this.subscriptions).forEach(subscription => subscription.unsubscribe());
  }

  private handleIncomingMessage(messageEvent: MessageEvent): void {
    const msgData = messageEvent.data;
    const msgType = msgData.type;
    let msgPlayerId = msgData.sessionId;
    if ((msgPlayerId === undefined) || (msgPlayerId === null)) {
      msgPlayerId = this.itemplayerSessionId;
    }

    switch (msgType) {
      case 'vopReadyNotification':
        // TODO add apiVersion check
        if (!this.pendingUnitData || this.pendingUnitData.playerId !== msgPlayerId) {
          this.pendingUnitData = {
            unitDefinition: '',
            unitDataParts: '',
            playerId: '',
            currentPage: null
          };
        }
        if (this.tcs.testMode.saveResponses) {
          this.bs.updateUnitState(this.tcs.testId, this.currentUnit.unitDef.alias, [<StateReportEntry>{
            key: UnitStateKey.PLAYER, timeStamp: Date.now(), content: UnitPlayerState.RUNNING
          }]);
        }
        this.postMessageTarget = messageEvent.source as Window;

        this.postMessageTarget.postMessage({
          type: 'vopStartCommand',
          sessionId: this.itemplayerSessionId,
          unitDefinition: this.pendingUnitData.unitDefinition,
          unitState: {
            dataParts: { all: this.pendingUnitData.unitDataParts }
          },
          playerConfig: this.getPlayerConfig()
        }, '*');

        // TODO maybe clean up memory?

        break;

      case 'vopStateChangedNotification':
        if (msgPlayerId === this.itemplayerSessionId) {
          if (msgData.playerState) {
            const { playerState } = msgData;

            this.knownPages = Object.keys(playerState.validPages)
              .map(id => ({ id, label: playerState.validPages[id] }));
            this.currentPageIndex = this.knownPages.findIndex(page => page.id === playerState.currentPage);

            if (typeof playerState.currentPage !== 'undefined') {
              const pageId = playerState.currentPage;
              const pageNr = this.knownPages.indexOf(playerState.currentPage) + 1;
              const pageCount = this.knownPages.length;
              if (this.knownPages.length > 1 && this.knownPages.indexOf(playerState.currentPage) >= 0) {
                this.tcs.newUnitStateCurrentPage(
                  this.currentUnit.unitDef.alias,
                  this.currentUnitSequenceId,
                  pageNr,
                  pageId,
                  pageCount
                );
              }
            }
          }
          const unitDbKey = this.currentUnit.unitDef.alias;
          if (msgData.unitState) {
            const { unitState } = msgData;
            const { presentationProgress, responseProgress } = unitState;

            if (presentationProgress) {
              this.tcs.updateUnitStatePresentationProgress(unitDbKey, this.currentUnitSequenceId, presentationProgress);
            }

            if (responseProgress) {
              this.tcs.newUnitStateResponseProgress(unitDbKey, this.currentUnitSequenceId, responseProgress);
            }

            const unitDataPartsAll = unitState?.dataParts?.all;
            if (unitDataPartsAll) {
              this.tcs.newUnitStateData(unitDbKey, this.currentUnitSequenceId,
                unitDataPartsAll, unitState.unitStateDataType);
            }
          }
          if (msgData.log) {
            this.bs.addUnitLog(this.tcs.testId, unitDbKey, msgData.log);
          }
        }
        break;

      case 'vopUnitNavigationRequestedNotification':
        if (msgPlayerId === this.itemplayerSessionId) {
          // support Verona2 and Verona3 version
          const target = msgData.target ? `#${msgData.target}` : msgData.targetRelative;
          this.tcs.setUnitNavigationRequest(target);
        }
        break;

      case 'vopWindowFocusChangedNotification':
        if (msgData.hasFocus) {
          this.tcs.windowFocusState$.next(WindowFocusState.PLAYER);
        } else if (document.hasFocus()) {
          this.tcs.windowFocusState$.next(WindowFocusState.HOST);
        } else {
          this.tcs.windowFocusState$.next(WindowFocusState.UNKNOWN);
        }
        break;

      default:
        console.log(`processMessagePost ignored message: ${msgType}`);
        break;
    }
  }

  private open(currentUnitSequenceId: number): void {
    this.currentUnitSequenceId = currentUnitSequenceId;
    this.tcs.currentUnitSequenceId = this.currentUnitSequenceId;
    this.mds.appSubTitle$.next(`Aufgabe ${this.currentUnitSequenceId}`);

    while (this.iFrameHostElement.hasChildNodes()) {
      this.iFrameHostElement.removeChild(this.iFrameHostElement.lastChild);
    }

    this.currentPageIndex = undefined;
    this.knownPages = [];

    this.currentUnit = this.tcs.rootTestlet.getUnitAt(this.currentUnitSequenceId);

    if (this.subscriptions.loading) {
      this.subscriptions.loading.unsubscribe();
    }

    const unitsToLoadIds = this.currentUnit.maxTimerRequiringTestlet ?
      this.tcs.rootTestlet.getAllUnitSequenceIds(this.currentUnit.maxTimerRequiringTestlet?.id) :
      [currentUnitSequenceId];

    const unitsToLoad = unitsToLoadIds
      .map(unitSequenceId => this.tcs.getUnitLoadProgress$(unitSequenceId));

    this.unitsToLoadLabels = unitsToLoadIds
      .map(unitSequenceId => this.tcs.rootTestlet.getUnitAt(unitSequenceId).unitDef.title);

    this.subscriptions.loading = combineLatest<LoadingProgress[]>(unitsToLoad)
      .subscribe({
        next: value => {
          this.unitsLoading$.next(value);
        },
        error: err => {
          this.mds.appError$.next({
            label: `Unit konnte nicht geladen werden. ${err.info}`, // TODO which item failed?
            description: (err.info) ? err.info : err,
            category: 'PROBLEM'
          });
        },
        complete: () => this.prepareUnit()
      });
  }

  private prepareUnit(): void {
    this.unitsLoading$.next([]);
    this.tcs.currentUnitDbKey = this.currentUnit.unitDef.alias;
    this.tcs.currentUnitTitle = this.currentUnit.unitDef.title;

    if (this.tcs.testMode.saveResponses) {
      this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
        key: TestStateKey.CURRENT_UNIT_ID, timeStamp: Date.now(), content: this.currentUnit.unitDef.alias
      }]);
      this.bs.updateUnitState(this.tcs.testId, this.currentUnit.unitDef.alias, [<StateReportEntry>{
        key: UnitStateKey.PLAYER, timeStamp: Date.now(), content: UnitPlayerState.LOADING
      }]);
    }

    if (this.tcs.testMode.presetCode) {
      this.currentUnit.codeRequiringTestlets
        .forEach(testlet => { this.clearCodes[testlet.id] = testlet.codeToEnter; });
    }

    this.runUnit();
  }

  private runUnit(): void {
    this.codeRequiringTestlets = this.currentUnit.codeRequiringTestlets
      .filter(testlet => !this.tcs.clearCodeTestlets.includes(testlet.id));

    if (this.codeRequiringTestlets.length) {
      return;
    }

    if (this.currentUnit.unitDef.locked) {
      return;
    }

    this.startTimerIfNecessary();

    this.itemplayerSessionId = Math.floor(Math.random() * 20000000 + 10000000).toString();

    this.pendingUnitData = {
      playerId: this.itemplayerSessionId,
      unitDefinition: this.tcs.hasUnitDefinition(this.currentUnitSequenceId) ?
        this.tcs.getUnitDefinition(this.currentUnitSequenceId) :
        null,
      unitDataParts: this.tcs.hasUnitStateDataParts(this.currentUnitSequenceId) ?
        this.tcs.getUnitStateDataParts(this.currentUnitSequenceId) :
        null,
      currentPage: this.tcs.hasUnitStateCurrentPage(this.currentUnitSequenceId) ?
        this.tcs.getUnitStateCurrentPage(this.currentUnitSequenceId) :
        null
    };
    this.leaveWarning = false;

    this.prepareIframe();
  }

  private startTimerIfNecessary(): void {
    if (this.currentUnit.maxTimerRequiringTestlet === null) {
      return;
    }
    if (this.tcs.currentMaxTimerTestletId &&
      (this.currentUnit.maxTimerRequiringTestlet.id === this.tcs.currentMaxTimerTestletId)
    ) {
      return;
    }
    this.tcs.startMaxTimer(
      this.currentUnit.maxTimerRequiringTestlet.id,
      this.currentUnit.maxTimerRequiringTestlet.maxTimeLeft
    );
  }

  private prepareIframe(): void {
    this.iFrameItemplayer = <HTMLIFrameElement>document.createElement('iframe');
    this.iFrameItemplayer.setAttribute('sandbox', 'allow-forms allow-scripts allow-same-origin');
    this.iFrameItemplayer.setAttribute('class', 'unitHost');
    this.adjustIframeSize();
    this.iFrameHostElement.appendChild(this.iFrameItemplayer);
    srcDoc.set(this.iFrameItemplayer, this.tcs.getPlayer(this.currentUnit.unitDef.playerId));
  }

  private adjustIframeSize(): void {
    this.iFrameItemplayer.setAttribute('height', String(this.iFrameHostElement.clientHeight));
  }

  @HostListener('window:resize')
  onResize(): void {
    if (this.iFrameItemplayer && this.iFrameHostElement) {
      this.adjustIframeSize();
    }
  }

  private getPlayerConfig(): VeronaPlayerConfig {
    const playerConfig: VeronaPlayerConfig = {
      enabledNavigationTargets: UnithostComponent.getEnabledNavigationTargets(
        this.currentUnitSequenceId,
        1,
        this.tcs.allUnitIds.length,
        this.tcs.bookletConfig.allow_player_to_terminate_test
      ),
      logPolicy: this.tcs.bookletConfig.logPolicy,
      pagingMode: this.tcs.bookletConfig.pagingMode,
      stateReportPolicy: this.tcs.bookletConfig.stateReportPolicy,
      unitNumber: this.currentUnitSequenceId,
      unitTitle: this.tcs.currentUnitTitle,
      unitId: this.currentUnit.unitDef.alias
    };
    if (this.pendingUnitData.currentPage && (this.tcs.bookletConfig.restore_current_page_on_return === 'ON')) {
      playerConfig.startPage = this.pendingUnitData.currentPage;
    }
    return playerConfig;
  }

  private static getEnabledNavigationTargets(
    nr: number, min: number, max: number,
    terminationAllowed: 'ON' | 'OFF' | 'LAST_UNIT' = 'ON'
  ): VeronaNavigationTarget[] {
    const navigationTargets = [];
    if (nr < max) {
      navigationTargets.push('next');
    }
    if (nr > min) {
      navigationTargets.push('previous');
    }
    if (nr !== min) {
      navigationTargets.push('first');
    }
    if (nr !== max) {
      navigationTargets.push('last');
    }
    if (terminationAllowed === 'ON') {
      navigationTargets.push('end');
    }
    if ((terminationAllowed === 'LAST_UNIT') && (nr === max)) {
      navigationTargets.push('end');
    }

    return navigationTargets;
  }

  gotoPage(navigationTarget: string): void {
    if (typeof this.postMessageTarget !== 'undefined') {
      this.postMessageTarget.postMessage({
        type: 'vopPageNavigationCommand',
        sessionId: this.itemplayerSessionId,
        target: navigationTarget
      }, '*');
    }
  }

  private handleNavigationDenial(
    navigationDenial: { sourceUnitSequenceId: number; reason: VeronaNavigationDeniedReason[] }
  ): void {
    if (navigationDenial.sourceUnitSequenceId !== this.currentUnitSequenceId) {
      return;
    }

    this.postMessageTarget.postMessage({
      type: 'vopNavigationDeniedNotification',
      sessionId: this.itemplayerSessionId,
      reason: navigationDenial.reason
    }, '*');
  }

  verifyCodes(): void {
    this.currentUnit.codeRequiringTestlets
      .forEach(
        testlet => {
          if (!this.clearCodes[testlet.id]) {
            return;
          }
          if (testlet.codeToEnter.toUpperCase().trim() === this.clearCodes[testlet.id].toUpperCase().trim()) {
            this.tcs.addClearedCodeTestlet(testlet.id);
            this.runUnit();
          } else {
            this.snackBar.open(
              `Freigabewort '${this.clearCodes[testlet.id]}' für '${testlet.title}' stimmt nicht.`,
              'OK',
              { duration: 3000 }
            );
            delete this.clearCodes[testlet.id];
          }
        }
      );
  }

  onKeydownInClearCodeInput($event: KeyboardEvent): void {
    if ($event.key === 'Enter') {
      this.verifyCodes();
    }
  }
}
<div
    [class]="{
      'unit-host': true,
      'with-header': tcs.bookletConfig.unit_screenheader !== 'OFF',
      'with-title': tcs.bookletConfig.unit_title === 'ON',
      'with-footer': tcs.bookletConfig.page_navibuttons === 'SEPARATE_BOTTOM',
      'is-waiting': currentUnit?.unitDef?.locked || (unitsLoading$ | async).length || codeRequiringTestlets.length
  }"
>
  <div
    *ngIf="tcs.bookletConfig.unit_title === 'ON'"
    id="unit-title"
    fxLayout="column"
    fxLayoutAlign="center center"
  >
    <p>{{currentUnit?.unitDef?.title}}</p>
  </div>

  <div id="iframe-host">
  </div>

  <ng-container *ngIf="{ list: (unitsLoading$ | async) } as loadingUnits">
    <div id="waiting-room" *ngIf="loadingUnits.list.length || currentUnit?.unitDef?.locked || codeRequiringTestlets.length">
      <mat-card>
        <mat-card-title>{{currentUnit?.unitDef?.title}}</mat-card-title>
        <mat-card-subtitle *ngIf="loadingUnits.list.length > 1">
          {{'Aufgabenblock wird geladen' | customtext:'booklet_loadingBlock' | async}}
        </mat-card-subtitle>
        <mat-card-subtitle *ngIf="loadingUnits.list.length === 1">
          {{'Aufgabe wird geladen' | customtext:'booklet_loadingUnit' | async}}
        </mat-card-subtitle>
        <mat-card-subtitle *ngIf="currentUnit?.unitDef?.locked">
          {{'Aufgabenzeit ist Abgelaufen' | customtext:'booklet_lockedBlock' | async}}
        </mat-card-subtitle>
        <mat-card-subtitle *ngIf="currentUnit.codeRequiringTestlets.length">
          {{'Aufgabenblock erfordert ein Freigabewort' | customtext:'booklet_codeToEnterTitle' | async}}
        </mat-card-subtitle>

        <mat-card-content *ngIf="codeRequiringTestlets.length">
          <ng-container *ngFor="let testlet of codeRequiringTestlets">
            <mat-form-field style="display: block">
              <mat-label>{{testlet.title || ('Block ' + (testlet.sequenceId + 1))}}</mat-label>
              <input
                matInput
                type="text"
                [(ngModel)]="clearCodes[testlet.id]"
                style="text-transform:uppercase"
                (keydown)="onKeydownInClearCodeInput($event)"
              >
              <mat-hint align="start" *ngIf="testlet.codePrompt">{{testlet.codePrompt}}</mat-hint>
              <mat-hint *ngIf="!testlet.codePrompt">
                {{'Bitte Freigabewort eingeben!' | customtext:'booklet_codeToEnterPrompt' | async}}
              </mat-hint>
            </mat-form-field>
          </ng-container>
        </mat-card-content>
        <mat-card-content>
          <ng-container *ngFor="let loading of loadingUnits.list; let index = index">
            <mat-progress-bar
              color="primary"
              [mode]="({'UNKNOWN': 'indeterminate', 'PENDING': 'query'})[loading.progress] || 'determinate'"
              [value]="loading.progress"
            >
            </mat-progress-bar>
            <p class="progress-bar-sub-text">
              {{unitsToLoadLabels[index]}}
              <ng-container [ngSwitch]="loading.progress">
                <span *ngSwitchCase="'UNKNOWN'">
                  ({{'wird geladen' | customtext:'booklet_unitLoadingUnknownProgress' | async}})
                </span>
                <span *ngSwitchCase="'PENDING'">
                  ({{'in der Warteschleife' | customtext:'booklet_unitLoadingPending' | async}})
                </span>
                <span *ngSwitchDefault>
                  ({{loading.progress}}% {{'geladen' | customtext:'booklet_unitLoading' | async}})
                </span>
              </ng-container>
            </p>
          </ng-container>
        </mat-card-content>

        <mat-card-actions>
          <button
            *ngIf="codeRequiringTestlets.length"
            mat-raised-button
            color="primary"
            [disabled]="(clearCodes | keyvalue)?.length < codeRequiringTestlets.length"
            (click)="verifyCodes()"
          >
            OK
          </button>
          <button
            *ngIf="tcs.bookletConfig.unit_navibuttons === 'OFF'"
            mat-raised-button
            [disabled]="tcs.currentUnitSequenceId === 0"
            (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.PREVIOUS)" matTooltip="Zurück" fxFlex="none"
          >
            <i class="material-icons">chevron_left</i>
          </button>
          <button
            *ngIf="tcs.bookletConfig.unit_navibuttons === 'OFF'"
            mat-raised-button
            [disabled]="tcs.currentUnitSequenceId >= tcs.allUnitIds.length"
            (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.NEXT)" matTooltip="Weiter" fxFlex="none"
          >
            <i class="material-icons">chevron_right</i>
          </button>
        </mat-card-actions>
      </mat-card>
    </div>
  </ng-container>

  <div
    id="page-navigation"
    fxLayout="row"
    fxLayoutAlign="end center"
    fxLayoutGap="10px"
    *ngIf="tcs.bookletConfig.page_navibuttons === 'SEPARATE_BOTTOM'"
  >
    <div fxLayout="row" fxLayoutAlign="space-between center" *ngIf="knownPages && knownPages.length">
      <div id="page-navigation-prompt">
        {{ ''  | customtext:'login_pagesNaviPrompt' | async}}
      </div>

      <mat-button-toggle-group>
        <mat-button-toggle
          [disabled]="currentPageIndex <= 0"
          (click)="gotoPage(knownPages[currentPageIndex - 1].id);"
          fxLayout="row"
          fxLayoutAlign="center center"
        >
          <i class="material-icons">chevron_left</i>
        </mat-button-toggle>

        <mat-button-toggle
          *ngFor="let page of knownPages; let index = index"
          fxLayout="column"
          (click)="gotoPage(page.id)"
          [matTooltip]="page.label"
          [checked]="currentPageIndex === index"
        >
          {{ index + 1 }}
        </mat-button-toggle>

        <mat-button-toggle
          [disabled]="currentPageIndex >= knownPages.length - 1"
          (click)="gotoPage(knownPages[currentPageIndex + 1].id);"
          fxLayout="row"
          fxLayoutAlign="center center"
        >
          <i class="material-icons">chevron_right</i>
        </mat-button-toggle>
      </mat-button-toggle-group>
    </div>
  </div>
</div>

./unithost.component.css

:host {
  width: 100%
}

#iframe-host {
  position: fixed;
  width: 100%;
  top: 0;
  bottom: 0;
  padding: 0;
  background-color: white;
}

.with-header #iframe-host {
  top: 65px;
}

.with-title #iframe-host {
  top: 40px;
}

.with-header.with-title #iframe-host {
  top: 105px;
}

.with-footer #iframe-host {
  bottom: 45px;
}

#waiting-room {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
  max-width: 50%;
}

#waiting-room mat-card {
  margin: 4em;
}

#waiting-room .progress-bar-sub-text {
  margin-top: 0.1em
}

#unit-title {
  position: absolute;
  width: 100%;
  top: 0;
  height: 39px;
  font-size: 1.5em;
  background-color: white;
  border-bottom: solid 1px black;
  white-space: nowrap;
  text-overflow: clip;
  overflow: hidden;
}

.is-waiting #iframe-host,
.is-waiting #unit-title {
  visibility: hidden;
}

#page-navigation {
  position: absolute;
  width: 100%;
  height: 45px;
  bottom: 0;
  padding: 0 16px;
  font-size: 1.2em;
}

#page-navigation-prompt {
  padding-right: 8px;
  color: white;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""