File

src/app/app.component.ts

Description

This is the main angular component that all the other components branch off from. It is in charge of the header and drawer components who have many sub-components.

Implements

OnInit

Metadata

Index

Properties
Methods
Accessors

Constructor

constructor(el: ElementRef, injector: Injector, data: DataState, theming: ThemingService, scene: SceneState, listResultsState: ListResultsState, consentService: ConsentService, snackbar: MatSnackBar, overlay: AppRootOverlayContainer, dataSource: DataSourceService, globalConfig: GlobalConfigState<AppOptions>, cdr: ChangeDetectorRef)

Creates an instance of app component.

Parameters :
Name Type Optional Description
el ElementRef<HTMLElement> No
injector Injector No
data DataState No

The data state.

theming ThemingService No
scene SceneState No
listResultsState ListResultsState No
consentService ConsentService No
snackbar MatSnackBar No
overlay AppRootOverlayContainer No
dataSource DataSourceService No
globalConfig GlobalConfigState<AppOptions> No
cdr ChangeDetectorRef No

Methods

asMutable
asMutable(value: Immutable)
Type parameters :
  • T
Parameters :
Name Type Optional
value Immutable<T> No
Returns : T
closeiFrameViewer
closeiFrameViewer()

Function to easily close the iFrame viewer.

Returns : void
createSelectionLabel
createSelectionLabel(ontologySelection: OntologySelection[])

Creates selection label for the results-browser to display based on an array of selected ontology nodes.

Parameters :
Name Type Optional
ontologySelection OntologySelection[] No
Returns : string
isItemSelected
isItemSelected(item: string)
Parameters :
Name Type Optional
item string No
Returns : any
ontologySelected
ontologySelected(ontologySelection: OntologySelection[] | undefined, type: "anatomical-structures" | "cell-type" | "biomarkers")

Captures changes in the ontologySelection and uses them to update the results-browser label and the filter object in the data store.

Parameters :
Name Type Optional Description
ontologySelection OntologySelection[] | undefined No

the list of currently selected organ nodes

type "anatomical-structures" | "cell-type" | "biomarkers" No
Returns : void
openiFrameViewer
openiFrameViewer(url: string)

Opens the iframe viewer with an url

Parameters :
Name Type Optional Description
url string No

The url

Returns : void
reset
reset(left: DrawerComponent, right: DrawerComponent, filterbox: FiltersPopoverComponent)

Resets the drawers and filter components to their default state.

Parameters :
Name Type Optional Description
left DrawerComponent No

The left drawer component gets passed in so we can call it's methods to control it's state

right DrawerComponent No

The right drawer component gets passed in so we can call it's methods to control it's state

filterbox FiltersPopoverComponent No

The filter's popover component gets passed in so we can control it's popover's state

Returns : void
resetView
resetView()
Returns : void
toggleScheme
toggleScheme()

Toggles scheme between light and dark mode

Returns : void
toggleSelection
toggleSelection(value: string[])
Parameters :
Name Type Optional
value string[] No
Returns : void

Properties

acceptableViewerDomains
Type : string[]
Default value : environment.acceptableViewerDomains || []

Acceptable viewer domains (others will open in new window)

Readonly baseHref$
Default value : this.globalConfig.getOption('baseHref')
biomarkerSelectionLabel
Type : string
Default value : 'biomarker'
Readonly biomarkersTreeModel$
Type : Observable<OntologyTreeModel>
Decorators :
@Select(DataStateSelectors.biomarkersTreeModel)
Readonly biomarkerTerms$
Type : Observable<string[]>
bodyUI
Type : BodyUiComponent
Decorators :
@ViewChild('bodyUI', {static: false})
cellTypeSelectionLabel
Type : string
Default value : 'cell'
Readonly cellTypeTerms$
Type : Observable<string[]>
Readonly cellTypeTreeModel$
Type : Observable<OntologyTreeModel>
Decorators :
@Select(DataStateSelectors.cellTypesTreeModel)
Readonly filter$
Default value : this.globalConfig.getOption('filter')
Readonly header$
Default value : this.globalConfig.getOption('header')
Readonly homeUrl$
Default value : this.globalConfig.getOption('homeUrl')
Readonly loadingMessage$
Default value : this.data.state$.pipe(map((x) => x?.statusMessage))
Readonly loginDisabled$
Default value : this.globalConfig.getOption('loginDisabled')
Readonly logoTooltip$
Default value : this.globalConfig.getOption('logoTooltip')
menuOptions
Type : string[]
Default value : ['AS', 'CT', 'B']
ontologySelectionLabel
Type : string
Default value : 'body'

Used to keep track of the ontology label to be passed down to the results-browser component.

Readonly ontologyTerms$
Type : Observable<string[]>
Readonly ontologyTreeModel$
Type : Observable<OntologyTreeModel>
Decorators :
@Select(DataStateSelectors.anatomicalStructuresTreeModel)
organListVisible
Default value : true

Whether or not organ carousel is open

Readonly removeSpatialSearch
Default value : actionAsFn(RemoveSearch)
Decorators :
@Dispatch()
Readonly selectableSearches$
Type : Observable<SpatialSearchFilterItem>
Decorators :
@Select(SpatialSearchFilterSelectors.items)
Readonly selectedOrgans$
Default value : this.globalConfig.getOption('selectedOrgans')
selectedtoggleOptions
Type : string[]
Default value : []
selectionLabel
Type : string
Default value : 'body | cell | biomarker'
Readonly setSelectedSearches
Default value : actionAsFn(SetSelectedSearches)
Decorators :
@Dispatch()
Readonly spinnerActive$
Default value : this.data.queryStatus$.pipe(map((state) => state === DataQueryState.Running))

Emits true whenever the overlay spinner should activate.

Readonly theme$
Default value : this.globalConfig.getOption('theme')
Readonly themeMode$
Default value : new ReplaySubject<'light' | 'dark'>(1)
tooltips
Type : string[]
Default value : ['Anatomical Structures', 'Cell Types', 'Biomarkers']
url
Type : string
Default value : ''

Emitted url object from the results browser item

viewerOpen
Default value : false

Variable to keep track of whether the viewer is open or not

Accessors

isLightTheme
getisLightTheme()
isFirefox
getisFirefox()
loggedIn
getloggedIn()

Gets login token

Returns : boolean
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ElementRef,
  Injector,
  OnInit,
  ViewChild,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Dispatch } from '@ngxs-labs/dispatch-decorator';
import { Select } from '@ngxs/store';
import { CCFDatabaseOptions, Filter, OntologyTreeModel } from 'ccf-database';
import { BodyUiComponent, DataSourceService, GlobalConfigState, OrganInfo, TrackingPopupComponent } from 'ccf-shared';
import { ConsentService } from 'ccf-shared/analytics';
import { Observable, ReplaySubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

import { Immutable } from '@angular-ru/common/typings';
import { environment } from '../environments/environment';
import { OntologySelection } from './core/models/ontology-selection';
import { AppRootOverlayContainer } from './core/services/app-root-overlay/app-root-overlay.service';
import { ThemingService } from './core/services/theming/theming.service';
import { actionAsFn } from './core/store/action-as-fn';
import { DataStateSelectors } from './core/store/data/data.selectors';
import { DataQueryState, DataState } from './core/store/data/data.state';
import { ListResultsState } from './core/store/list-results/list-results.state';
import { SceneState } from './core/store/scene/scene.state';
import { RemoveSearch, SetSelectedSearches } from './core/store/spatial-search-filter/spatial-search-filter.actions';
import { SpatialSearchFilterSelectors } from './core/store/spatial-search-filter/spatial-search-filter.selectors';
import { SpatialSearchFilterItem } from './core/store/spatial-search-filter/spatial-search-filter.state';
import { FiltersPopoverComponent } from './modules/filters/filters-popover/filters-popover.component';
import { DrawerComponent } from './shared/components/drawer/drawer/drawer.component';

interface AppOptions extends CCFDatabaseOptions {
  theme?: string;
  header?: boolean;
  homeUrl?: string;
  logoTooltip?: string;
  selectedOrgans?: string[];
  loginEnabled?: boolean;
  baseHref?: string;
  filter?: Partial<Filter>;
  loginDisabled?: boolean;
}

/**
 * This is the main angular component that all the other components branch off from.
 * It is in charge of the header and drawer components who have many sub-components.
 */
@Component({
  selector: 'ccf-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
  @ViewChild('bodyUI', { static: false }) bodyUI!: BodyUiComponent;

  @Select(DataStateSelectors.cellTypesTreeModel)
  readonly cellTypeTreeModel$!: Observable<OntologyTreeModel>;

  @Select(DataStateSelectors.anatomicalStructuresTreeModel)
  readonly ontologyTreeModel$!: Observable<OntologyTreeModel>;

  @Select(DataStateSelectors.biomarkersTreeModel)
  readonly biomarkersTreeModel$!: Observable<OntologyTreeModel>;

  @Select(SpatialSearchFilterSelectors.items)
  readonly selectableSearches$!: Observable<SpatialSearchFilterItem>;

  @Dispatch()
  readonly setSelectedSearches = actionAsFn(SetSelectedSearches);

  @Dispatch()
  readonly removeSpatialSearch = actionAsFn(RemoveSearch);

  menuOptions: string[] = ['AS', 'CT', 'B'];
  tooltips: string[] = ['Anatomical Structures', 'Cell Types', 'Biomarkers'];
  /**
   * Used to keep track of the ontology label to be passed down to the
   * results-browser component.
   */
  ontologySelectionLabel = 'body';

  cellTypeSelectionLabel = 'cell';

  biomarkerSelectionLabel = 'biomarker';

  selectionLabel = 'body | cell | biomarker';

  selectedtoggleOptions: string[] = [];

  /**
   * Whether or not organ carousel is open
   */
  organListVisible = true;

  /**
   * Emitted url object from the results browser item
   */
  url = '';

  /**
   * Acceptable viewer domains (others will open in new window)
   */
  acceptableViewerDomains: string[] = environment.acceptableViewerDomains || [];

  /**
   * Variable to keep track of whether the viewer is open
   * or not
   */
  viewerOpen = false;

  get isLightTheme(): boolean {
    return this.theming.getTheme().endsWith('light');
  }

  get isFirefox(): boolean {
    return navigator.userAgent.indexOf('Firefox') !== -1;
  }

  /** Emits true whenever the overlay spinner should activate. */
  readonly spinnerActive$ = this.data.queryStatus$.pipe(map((state) => state === DataQueryState.Running));

  readonly loadingMessage$ = this.data.state$.pipe(map((x) => x?.statusMessage));

  readonly ontologyTerms$: Observable<readonly string[]>;
  readonly cellTypeTerms$: Observable<readonly string[]>;
  readonly biomarkerTerms$: Observable<readonly string[]>;

  readonly theme$ = this.globalConfig.getOption('theme');
  readonly themeMode$ = new ReplaySubject<'light' | 'dark'>(1);

  readonly header$ = this.globalConfig.getOption('header');
  readonly homeUrl$ = this.globalConfig.getOption('homeUrl');
  readonly logoTooltip$ = this.globalConfig.getOption('logoTooltip');
  readonly loginDisabled$ = this.globalConfig.getOption('loginDisabled');
  readonly filter$ = this.globalConfig.getOption('filter');
  readonly selectedOrgans$ = this.globalConfig.getOption('selectedOrgans');
  readonly baseHref$ = this.globalConfig.getOption('baseHref');

  /**
   * Creates an instance of app component.
   *
   * @param data The data state.
   */
  constructor(
    el: ElementRef<HTMLElement>,
    injector: Injector,
    readonly data: DataState,
    readonly theming: ThemingService,
    readonly scene: SceneState,
    readonly listResultsState: ListResultsState,
    readonly consentService: ConsentService,
    readonly snackbar: MatSnackBar,
    overlay: AppRootOverlayContainer,
    readonly dataSource: DataSourceService,
    private readonly globalConfig: GlobalConfigState<AppOptions>,
    cdr: ChangeDetectorRef,
  ) {
    theming.initialize(el, injector);
    overlay.setRootElement(el);
    data.tissueBlockData$.subscribe();
    data.aggregateData$.subscribe();
    data.ontologyTermOccurencesData$.subscribe();
    data.cellTypeTermOccurencesData$.subscribe();
    data.biomarkerTermOccurencesData$.subscribe();
    data.sceneData$.subscribe();
    data.filter$.subscribe();
    data.technologyFilterData$.subscribe();
    data.providerFilterData$.subscribe();
    this.ontologyTerms$ = data.filter$.pipe(map((x) => x?.ontologyTerms));
    this.cellTypeTerms$ = data.filter$.pipe(map((x) => x?.cellTypeTerms));
    this.biomarkerTerms$ = data.filter$.pipe(map((x) => x?.biomarkerTerms));
    this.filter$.subscribe((filter = {}) => data.updateFilter(filter));
    this.baseHref$.subscribe((ref) => this.globalConfig.patchState({ baseHref: ref ?? '' }));

    combineLatest([scene.referenceOrgans$, this.selectedOrgans$]).subscribe(([refOrgans, selected]) => {
      scene.setSelectedReferenceOrgansWithDefaults(refOrgans as OrganInfo[], selected ?? []);
    });
    combineLatest([this.theme$, this.themeMode$]).subscribe(([theme, mode]) => {
      this.theming.setTheme(`${theme}-theme-${mode}`);
      cdr.markForCheck();
    });
    this.selectedtoggleOptions = this.menuOptions;
  }

  ngOnInit(): void {
    const snackBar = this.snackbar.openFromComponent(TrackingPopupComponent, {
      data: {
        preClose: () => {
          snackBar.dismiss();
        },
      },
      duration: this.consentService.consent === 'not-set' ? Infinity : 3000,
    });

    if (window.matchMedia) {
      // Sets initial theme according to user theme preference
      if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
        this.themeMode$.next('dark');
      } else {
        this.themeMode$.next('light');
      }

      // Listens for changes in user theme preference
      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
        this.themeMode$.next(e.matches ? 'dark' : 'light');
      });
    } else {
      this.themeMode$.next('light');
    }
  }

  /**
   * Resets the drawers and filter components to their default state.
   *
   * @param left The left drawer component gets passed in so we can call it's methods to control it's state
   * @param right The right drawer component gets passed in so we can call it's methods to control it's state
   * @param filterbox The filter's popover component gets passed in so we can control it's popover's state
   */
  reset(left: DrawerComponent, right: DrawerComponent, filterbox: FiltersPopoverComponent): void {
    left.open();
    left.closeExpanded();
    right.open();
    right.closeExpanded();
    filterbox.removeBox();
    this.resetView();
  }

  resetView(): void {
    this.bodyUI.target = [0, 0, 0];
    this.bodyUI.rotation = 0;
    this.bodyUI.rotationX = 0;
    this.bodyUI.bounds = { x: 2.2, y: 2, z: 0.4 };
  }

  /**
   * Toggles scheme between light and dark mode
   */
  toggleScheme(): void {
    this.themeMode$.next(this.isLightTheme ? 'dark' : 'light');
  }

  /**
   * Captures changes in the ontologySelection and uses them to update the results-browser label
   * and the filter object in the data store.
   *
   * @param ontologySelection the list of currently selected organ nodes
   */
  ontologySelected(
    ontologySelection: OntologySelection[] | undefined,
    type: 'anatomical-structures' | 'cell-type' | 'biomarkers',
  ): void {
    if (ontologySelection) {
      if (type === 'anatomical-structures') {
        this.data.updateFilter({ ontologyTerms: ontologySelection.map((selection) => selection.id) });
        this.ontologySelectionLabel = this.createSelectionLabel(ontologySelection);
      } else if (type === 'cell-type') {
        this.data.updateFilter({ cellTypeTerms: ontologySelection.map((selection) => selection.id) });
        this.cellTypeSelectionLabel = this.createSelectionLabel(ontologySelection);
      } else if (type === 'biomarkers') {
        this.data.updateFilter({ biomarkerTerms: ontologySelection.map((selection) => selection.id) });
        this.biomarkerSelectionLabel = this.createSelectionLabel(ontologySelection);
      }

      this.selectionLabel = [
        this.ontologySelectionLabel || 'body',
        this.cellTypeSelectionLabel || 'cell',
        this.biomarkerSelectionLabel || 'biomarker',
      ].join(' | ');

      if (ontologySelection[0] && ontologySelection[0].label === 'body') {
        this.resetView();
      }
      return;
    }

    this.data.updateFilter({ ontologyTerms: [], cellTypeTerms: [], biomarkerTerms: [] });
    this.ontologySelectionLabel = '';
    this.cellTypeSelectionLabel = '';
  }

  /**
   * Creates selection label for the results-browser to display based on an
   * array of selected ontology nodes.
   */
  createSelectionLabel(ontologySelection: OntologySelection[]): string {
    if (ontologySelection.length === 0) {
      return '';
    }

    if (ontologySelection.length === 1) {
      return ontologySelection[0].label;
    }

    let selectionString = '';
    ontologySelection.forEach((selection, index) => {
      selectionString += selection.label;

      // Don't add a comma if it's the last item in the array.
      if (index < ontologySelection.length - 1) {
        selectionString += ', ';
      }
    });
    return selectionString;
  }

  /**
   * Opens the iframe viewer with an url
   *
   * @param url The url
   */
  openiFrameViewer(url: string): void {
    const isWhitelisted = this.acceptableViewerDomains.some((domain) => url?.startsWith(domain));
    if (isWhitelisted) {
      this.url = url;
      this.viewerOpen = !!url;
    } else {
      // Open link in new tab
      window.open(url, '_blank');
      this.closeiFrameViewer();
    }
  }

  /**
   * Function to easily close the iFrame viewer.
   */
  closeiFrameViewer(): void {
    this.viewerOpen = false;
  }

  /**
   * Gets login token
   */
  get loggedIn(): boolean {
    const token = this.globalConfig.snapshot.hubmapToken ?? '';
    return token.length > 0;
  }

  isItemSelected(item: string) {
    return this.selectedtoggleOptions.includes(item);
  }

  toggleSelection(value: string[]) {
    this.selectedtoggleOptions = value;
  }

  asMutable<T>(value: Immutable<T>): T {
    return value as T;
  }
}
<div class="ccf-app mat-app-background">
  <ccf-spinner-overlay [text]="(loadingMessage$ | async) ?? ''" [active]="(spinnerActive$ | async) ?? false">
  </ccf-spinner-overlay>
  <ccf-header
    [class.hide]="(header$ | async) === false"
    [logoTooltip]="(logoTooltip$ | async) ?? ''"
    [homeUrl]="(homeUrl$ | async) ?? ''"
    [loggedIn]="loggedIn"
    [loginDisabled]="(loginDisabled$ | async) ?? false"
    [baseRef]="(baseHref$ | async) ?? ''"
    *ngIf="(spinnerActive$ | async) === false"
  >
  </ccf-header>

  <ccf-drawer-container class="main-drawers" [class.header-hidden]="(header$ | async) === false">
    <ccf-drawer class="left-drawer" opened #left (stateChange)="filterbox.removeBox()">
      <div class="left-drawer-container">
        <div class="filter-data">
          <ccf-filters-popover
            [filters]="$any(data.filter$ | async)"
            [drawerExpanded]="right.expanded"
            (filtersChange)="data.updateFilter($event)"
            [technologyFilters]="(data.technologyFilterData$ | async) ?? []"
            [providerFilters]="(data.providerFilterData$ | async) ?? []"
            [spatialSearchFilters]="$any(selectableSearches$ | async)"
            (spatialSearchSelected)="setSelectedSearches($event)"
            (spatialSearchRemoved)="removeSpatialSearch($event)"
            #filterbox
          >
          </ccf-filters-popover>

          <div class="filter-text">
            <div class="sex filter-tag">
              Sex: <strong>{{ (data.filter$ | async)?.sex }}</strong>
            </div>
            <div class="age filter-tag">
              Age:
              <strong>
                {{ $any(data.filter$ | async)?.ageRange[0] }}-{{ $any(data.filter$ | async)?.ageRange[1] }}
              </strong>
            </div>
            <div class="bmi filter-tag">
              BMI:
              <strong>
                {{ $any(data.filter$ | async)?.bmiRange[0] }}-{{ $any(data.filter$ | async)?.bmiRange[1] }}
              </strong>
            </div>
          </div>
        </div>
        <ccf-button-toggle
          class="button-toggle-group"
          [menuOptions]="menuOptions"
          [enableTooltip]="true"
          [tooltips]="tooltips"
          [selectedItems]="selectedtoggleOptions"
          (selectionChange)="toggleSelection($event)"
        ></ccf-button-toggle>
        <div class="ontologies">
          <ccf-ontology-selection
            class="ontology-selection"
            [class.firefox]="isFirefox"
            *ngIf="isItemSelected('AS')"
            [showtoggle]="false"
            [treeModel]="(ontologyTreeModel$ | async)!"
            [termData]="(data.ontologyTermsFullData$ | async) ?? {}"
            [occurenceData]="(data.ontologyTermOccurencesData$ | async) ?? {}"
            placeholderText="Search anatomical structures..."
            (ontologySelection)="ontologySelected($any($event), 'anatomical-structures')"
            [header]="(header$ | async) ?? false"
          >
          </ccf-ontology-selection>
          <ccf-ontology-selection
            class="cell-type-selection"
            [class.firefox]="isFirefox"
            *ngIf="isItemSelected('CT')"
            [showtoggle]="false"
            [treeModel]="(cellTypeTreeModel$ | async)!"
            [termData]="(data.cellTypeTermsFullData$ | async) ?? {}"
            [occurenceData]="(data.cellTypeTermOccurencesData$ | async) ?? {}"
            placeholderText="Search cell types..."
            (ontologySelection)="ontologySelected($any($event), 'cell-type')"
            [header]="(header$ | async) ?? false"
          >
          </ccf-ontology-selection>
          <ccf-ontology-selection
            class="biomarker-selection"
            [class.firefox]="isFirefox"
            *ngIf="isItemSelected('B')"
            [showtoggle]="true"
            [treeModel]="(biomarkersTreeModel$ | async)!"
            [termData]="(data.biomarkerTermsFullData$ | async) ?? {}"
            [occurenceData]="(data.biomarkerTermOccurencesData$ | async) ?? {}"
            placeholderText="Search Biomarkers..."
            (ontologySelection)="ontologySelected($any($event), 'biomarkers')"
            [header]="(header$ | async) ?? false"
          >
          </ccf-ontology-selection>

          <div class="no-selection-notice" *ngIf="selectedtoggleOptions.length === 0">
            No anatomical structures, cell types, or biomarkers selected. Use the above AS, CT, and B buttons to view
            the registered listings.
          </div>
        </div>
        <ccf-drawer-toggle-button></ccf-drawer-toggle-button>
      </div>
    </ccf-drawer>
    <ccf-drawer class="right-drawer" position="end" opened #right (stateChange)="filterbox.removeBox()">
      <ccf-viewer class="portal-view" [class.opened]="viewerOpen" [url]="url" (closed)="viewerOpen = false">
      </ccf-viewer>

      <div class="drawer-icons">
        <div class="drawer-icons-left">
          <button
            class="button"
            (click)="filterbox.removeBox(); right.toggleExpanded()"
            [matTooltip]="right.expanded ? 'Exit Fullscreen' : 'Enter Fullscreen'"
          >
            <mat-icon class="icon">{{ right.expanded ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
          </button>
          <button
            class="scheme-toggle button"
            (click)="toggleScheme()"
            [matTooltip]="isLightTheme ? 'Enter Dark Mode' : 'Enter Light Mode'"
          >
            <mat-icon class="icon">{{ isLightTheme ? 'brightness_2' : 'brightness_5' }} </mat-icon>
          </button>
          <button class="button">
            <mat-icon class="refresh icon" (click)="reset(left, right, filterbox)" matTooltip="Reset View"
              >refresh
            </mat-icon>
          </button>
        </div>
        <ccf-info-button
          videoID="YAHJqvD3Q_8"
          infoTitle="HRA Exploration User Interface"
          [documentationUrl]="(baseHref$ | async) + 'assets/docs/README.md'"
          matTooltip="Open Info"
        >
        </ccf-info-button>
      </div>
      <ccf-results-browser
        [listResults]="(listResultsState.listResults$ | async) ?? []"
        [aggregateData]="(data.aggregateData$ | async) ?? []"
        [resultLabel]="selectionLabel"
        (listResultSelected)="listResultsState.selectListResult(asMutable($event))"
        (listResultDeselected)="listResultsState.deselectListResult(asMutable($event))"
        (linkClicked)="openiFrameViewer($event)"
        [highlighted]="(listResultsState.highlightedNodeId$ | async) ?? ''"
        (itemHovered)="listResultsState.highlightNode($event)"
        (itemUnhovered)="listResultsState.unHighlightNode()"
        [header]="(header$ | async) ?? false"
      >
      </ccf-results-browser>
      <ccf-drawer-toggle-button></ccf-drawer-toggle-button>
    </ccf-drawer>

    <ccf-drawer-content [class.header-hidden]="(header$ | async) === false">
      <div [class.closed]="!organListVisible" class="selector-drawer" [class.expanded]="selector.expanded">
        <ccf-organ-selector
          #selector
          class="organ-selector"
          [multiselect]="true"
          [occurenceData]="(data.ontologyTermOccurencesData$ | async) ?? {}"
          [organList]="$any(scene.referenceOrgans$ | async)"
          (organsChanged)="scene.setSelectedReferenceOrgans($event)"
          [selectedOrgans]="$any(scene.selectedReferenceOrgans$ | async)"
        >
        </ccf-organ-selector>
      </div>
      <div class="close-button-wrapper" [class.closed]="!organListVisible">
        <div *ngIf="organListVisible" class="close-button" (click)="selector.expanded = !selector.expanded">
          <mat-icon class="expand-collapse-icon" aria-hidden="false" aria-label="Expand carousel drawer">
            {{ selector.expanded ? 'arrow_drop_up' : 'arrow_drop_down' }}
          </mat-icon>
        </div>
        <div *ngIf="!selector.expanded" class="close-button" (click)="organListVisible = !organListVisible">
          <mat-icon class="expand-collapse-icon" aria-hidden="false" aria-label="Close carousel drawer">
            {{ organListVisible ? 'arrow_drop_up' : 'arrow_drop_down' }}
          </mat-icon>
        </div>
      </div>
      <ccf-run-spatial-search></ccf-run-spatial-search>
      <ccf-body-ui
        #bodyUI
        class="stage-content"
        [scene]="$any(scene.scene$ | async)"
        (nodeClick)="scene.sceneNodeClicked($event)"
        (nodeHoverStart)="scene.sceneNodeHovered($event)"
        (nodeHoverStop)="scene.sceneNodeUnhover()"
        [bounds]="{ x: 2.2, y: 2, z: 0.4 }"
        [class.expanded-stage]="!organListVisible"
        [class.selector-expanded]="selector.expanded"
      >
      </ccf-body-ui>
    </ccf-drawer-content>
  </ccf-drawer-container>
</div>

./app.component.scss

::ng-deep .cdk-overlay-container {
  position: absolute;
  font-size: 1.2rem;
}

:host {
  display: block;
  position: relative;
}

.ccf-app {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  font-size: 1rem;
  position: relative;
  text-align: left;

  ccf-header {
    z-index: 99;

    &.hide {
      display: none;
    }
  }

  ccf-drawer-container {
    height: calc(100% - 5rem);
    width: 100%;
    opacity: 1;
    overflow: hidden;
    transform: scale(1);

    &.header-hidden {
      height: 100%;
    }

    ccf-drawer {
      width: 28.5rem;

      div {
        .button-toggle-group {
          height: 4rem;
          display: flex;
          align-items: center;
        }
      }

      .drawer-icons {
        display: flex;
        padding-left: 1.5rem;
        height: 5rem;
        padding-right: 1.5rem;
        align-items: center;
        justify-content: space-between;

        .drawer-icons-left {
          display: flex;
          justify-content: flex-start;
          align-items: center;

          .button {
            padding: 0;
            border: none;
            cursor: pointer;
            outline: none;
            border-radius: 0.25rem;
            padding: 0.65rem;
            transition: 0.6s;
          }
        }
      }
    }

    ccf-drawer-content {
      overflow: hidden;
      border-radius: 0.5rem;
      height: calc(100vh - 4rem);

      .selector-drawer {
        top: 0rem;
        display: flex;
        flex-direction: column;
        position: relative;
        transition: all 0.5s ease-in-out;
        height: 5rem;
        justify-content: flex-start;

        &.expanded {
          height: 15rem;
          overflow-y: auto;
        }

        &.closed {
          height: 1.5rem;
          top: -5rem;
        }
      }

      ccf-run-spatial-search {
        position: relative;
        float: right;
        height: 0px;
        z-index: 1;
        top: 1.5rem;
        right: 0.5rem;
      }

      .stage-content {
        transition: all 0.5s ease-in-out;
        border-bottom-right-radius: 0.5rem;
        border-bottom-left-radius: 0.5rem;
      }

      .expanded-stage {
        height: calc(100% - 3rem);
      }

      .selector-expanded {
        height: calc(100% - 16.5rem);
      }

      &.header-hidden {
        height: calc(100vh - 1rem);
      }
    }

    .close-button-wrapper {
      display: flex;
      justify-content: center;
      border-top-right-radius: 0.5rem;
      border-top-left-radius: 0.5rem;
      height: 0;

      .close-button {
        display: flex;
        justify-content: center;
        height: 1.0625rem;
        width: 3rem;
        align-self: center;
        border-radius: 0rem 0rem 0.25rem 0.25rem;
        cursor: pointer;
        transition: 0.6s;
        position: relative;
        top: 0.5rem;
        z-index: 1;

        .mat-icon {
          position: relative;
          bottom: 0.2rem;
        }
      }
    }

    .left-drawer {
      padding-left: 1.5rem;
      padding-right: 1rem;

      .left-drawer-container {
        height: 100%;

        .ontologies {
          height: calc(100% - 9rem);
          display: flex;
          gap: 1.5rem;
          flex-direction: column;

          ccf-ontology-selection {
            flex: 1 1;
            overflow: auto;
            scrollbar-width: thin;
            &::-webkit-scrollbar {
              width: 0.25rem;
            }

            &.firefox {
              padding-right: 0.5rem;
            }

            &.biomarker-selection {
              flex-basis: 30%;
            }
          }

          .no-selection-notice {
            font-weight: 400;
            line-height: normal;
          }
        }
      }

      ::ng-deep .cff-drawer-inner-container {
        overflow: hidden;
      }

      .filter-data {
        height: 5rem;
        display: flex;
        align-items: center;

        .filter-text {
          display: flex;
          justify-content: space-between;
          width: 75%;
          margin-left: 1.5rem;

          .filter-tag {
            font-weight: 300;
            strong {
              font-weight: 600 !important;
            }
          }
        }
      }
    }
  }

  .right-drawer {
    .portal-view {
      position: fixed;
      left: 0;
      top: 0;
      bottom: 0;
      right: 425%;
      height: calc(100% + -1.5rem);
      transform: translateX(-425%);
      width: calc(100% - 28.5rem);

      transition: transform 0.5s cubic-bezier(0.82, 0.085, 0.395, 0.895);

      &.opened {
        transform: translateX(0);
        transition: width 0s 0.5s;
      }
    }

    &:not(.ccf-drawer-opened) .portal-view {
      width: 100vw;

      &.opened {
        transition: width 0s;
      }
    }

    .shaded-toggle {
      box-shadow: -1px 0 4px #212121;
    }

    ::ng-deep .cff-drawer-inner-container {
      overflow: hidden;
    }
  }
}

::-webkit-scrollbar {
  width: 0.5rem;
}

::ng-deep .mdc-snackbar__surface {
  box-shadow: none !important;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""