File

src/app/modules/search/search.component.ts

Metadata

Index

Properties
Methods
Inputs
HostListeners

Constructor

constructor(bms: BimodalService, store: Store, ga: GoogleAnalyticsService, router: Router, elementRef: ElementRef)
Parameters :
Name Type Optional
bms BimodalService No
store Store No
ga GoogleAnalyticsService No
router Router No
elementRef ElementRef No

Inputs

disabled
Type : boolean
Default value : false

HostListeners

document:click
Arguments : '$event'
document:click(event: MouseEvent)

Methods

clearSearchField
clearSearchField()
Returns : void
clickOutsideSearchList
clickOutsideSearchList(event: MouseEvent)
Decorators :
@HostListener('document:click', ['$event'])
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
closeSearchList
closeSearchList()
Returns : void
deselectAllOptions
deselectAllOptions()
Returns : void
Public filterStructuresOnSearch
filterStructuresOnSearch()
Returns : void
filterToggleChange
filterToggleChange(value: string[])
Parameters :
Name Type Optional
value string[] No
Returns : void
hideStructure
hideStructure(structure: SearchStructure)
Parameters :
Name Type Optional
structure SearchStructure No
Returns : boolean
isSelected
isSelected(structure: SearchStructure)
Parameters :
Name Type Optional
structure SearchStructure No
Returns : any
openSearchList
openSearchList()
Returns : void
selectAllOptions
selectAllOptions()
Returns : void
selectFirstOption
selectFirstOption()
Returns : void
selectOption
selectOption()
Returns : void

Properties

Public bms
Type : BimodalService
Public ga
Type : GoogleAnalyticsService
Public groupFilteredStructures
Type : SearchStructure[]
Default value : []
nodes
Type : BMNode[]
Default value : []
Public router
Type : Router
searchFieldContent
Type : ElementRef
Decorators :
@ViewChild('searchField', {static: false})
Public searchFilteredStructures
Type : SearchStructure[]
Default value : []
searchOpen
Default value : false
searchState$
Type : Observable<boolean>
Decorators :
@Select(UIState.getSearchState)
searchValue
Type : string
Default value : ''
selectedOptions
Type : SearchStructure[]
Default value : []
selectedValues
Type : string
Default value : ''
selectionCompareFunction
Default value : () => {...}
selectionMemory
Type : SearchStructure[]
Default value : []
sheetConfig
Type : SheetConfig
sheetConfig$
Type : Observable<SheetConfig>
Decorators :
@Select(SheetState.getSheetConfig)
Public store
Type : Store
Public structures
Type : SearchStructure[]
Default value : []
tree$
Type : Observable<TreeStateModel>
Decorators :
@Select(TreeState)
treeData
Type : TNode[]
Default value : []
ui$
Type : Observable<UIStateModel>
Decorators :
@Select(UIState)
import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Select, Store } from '@ngxs/store';
import { GoogleAnalyticsService } from 'ngx-google-analytics';
import { Observable } from 'rxjs';
import { UpdateConfig } from '../../actions/sheet.actions';
import { DiscrepencyId, DiscrepencyLabel, DoSearch, DuplicateId } from '../../actions/tree.actions';
import { CloseSearch, OpenSearch } from '../../actions/ui.actions';
import { BMNode } from '../../models/bimodal.model';
import { GaAction, GaCategory } from '../../models/ga.model';
import { SheetConfig } from '../../models/sheet.model';
import { SearchStructure, TNode } from '../../models/tree.model';
import { BimodalService } from '../../modules/tree/bimodal.service';
import { SheetState } from '../../store/sheet.state';
import { TreeState, TreeStateModel } from '../../store/tree.state';
import { UIState, UIStateModel } from '../../store/ui.state';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss'],
})
export class SearchComponent {
  @Input() disabled = false;

  // Structures contains the full list of structures to render for the search
  public structures: SearchStructure[] = [];
  // Contains the subset matching the search term, to hide filtered out
  // elements without removing them from the DOM completely
  public searchFilteredStructures: SearchStructure[] = [];
  // Contains the subset of structures matching the group name button toggle
  public groupFilteredStructures: SearchStructure[] = [];

  @ViewChild('searchField', { static: false }) searchFieldContent!: ElementRef;

  @Select(TreeState) tree$!: Observable<TreeStateModel>;
  @Select(UIState) ui$!: Observable<UIStateModel>;
  @Select(UIState.getSearchState) searchState$!: Observable<boolean>;
  @Select(SheetState.getSheetConfig) sheetConfig$!: Observable<SheetConfig>;

  treeData: TNode[] = [];
  nodes: BMNode[] = [];
  searchValue = '';
  selectedValues = '';
  selectedOptions: SearchStructure[] = [];
  selectionMemory: SearchStructure[] = [];
  sheetConfig!: SheetConfig;
  searchOpen = false;
  selectionCompareFunction = (o1: SearchStructure, o2: SearchStructure) => o1.id === o2.id;

  constructor(
    public bms: BimodalService,
    public store: Store,
    public ga: GoogleAnalyticsService,
    public router: Router,
    private readonly elementRef: ElementRef,
  ) {
    this.tree$.subscribe((tree) => {
      this.selectedOptions = tree.search;
      this.treeData = tree.treeData;
      this.nodes = tree.bimodal.nodes;
    });

    // On tree selection, reset the selected options and structures array
    this.router.events.subscribe((event) => {
      if (event instanceof NavigationEnd) {
        this.structures = [];
        this.selectedValues = '';
        this.selectedOptions = [];
        this.selectionMemory = [];
      }
    });

    this.sheetConfig$.subscribe((config) => {
      this.sheetConfig = config;
    });
  }

  selectOption() {
    // Find the latest option clicked
    const newSelections = this.selectedOptions.filter((item) => this.selectionMemory.indexOf(item) < 0);
    let lastClickedOption = null;
    if (newSelections.length > 0) {
      lastClickedOption = newSelections[0];
    }

    console.log(lastClickedOption);
    // Toggling the Discrepency fields to off
    this.sheetConfig.discrepencyId = false;
    this.sheetConfig.discrepencyLabel = false;
    this.sheetConfig.duplicateId = false;
    this.store.dispatch(new UpdateConfig(this.sheetConfig));
    // Dispatch the search data to the tree store
    this.store.dispatch(new DoSearch(this.selectedOptions, lastClickedOption as SearchStructure));

    // Clearing Discrepency fields so that searched options can appear
    this.store.dispatch(new DiscrepencyLabel([]));
    this.store.dispatch(new DiscrepencyId([]));
    this.store.dispatch(new DuplicateId([]));
    // Update the memory
    this.selectionMemory = this.selectedOptions.slice();
    // Build values for search bar UI text
    this.selectedValues = this.selectedOptions.map((obj) => obj.name).join(', ');

    this.ga.event(GaAction.CLICK, GaCategory.NAVBAR, 'Select/Deselect Search Filters');
  }

  selectFirstOption() {
    this.selectedOptions.push(this.searchFilteredStructures[0]);
    this.selectOption();
  }

  isSelected(structure: SearchStructure) {
    return this.selectedOptions.includes(structure);
  }

  deselectAllOptions() {
    this.selectedOptions = [];
    this.selectionMemory = [];
    this.selectedValues = '';
    this.store.dispatch(new DoSearch(this.selectedOptions, null as unknown as SearchStructure));
    this.ga.event(GaAction.CLICK, GaCategory.NAVBAR, 'Deselect All Search Filters');
  }

  selectAllOptions() {
    this.selectedOptions = this.groupFilteredStructures.filter((s) => this.searchFilteredStructures.indexOf(s) >= 0);
    this.selectionMemory = this.selectedOptions.slice();
    this.selectedValues = this.selectedOptions.map((obj) => obj.name).join(', ');
    this.store.dispatch(new DoSearch(this.selectedOptions, this.selectedOptions[0]));
    this.ga.event(GaAction.CLICK, GaCategory.NAVBAR, 'Select All Searched Options');
  }

  openSearchList() {
    if (this.structures.length === 0) {
      const searchSet = new Set<SearchStructure>();

      for (const node of this.treeData) {
        if (node.children !== 0) {
          searchSet.add({
            id: node.id,
            name: node.name,
            groupName: 'Anatomical Structures',
            x: node.x,
            y: node.y,
          });
        }
      }

      for (const node of this.nodes) {
        searchSet.add({
          id: node.id,
          name: node.name,
          groupName: node.groupName,
          x: node.x,
          y: node.y,
        });
      }

      this.structures = [...searchSet];
      this.searchFilteredStructures = this.structures.slice();
      this.groupFilteredStructures = this.structures.slice();
    }

    // Show search dropdown
    this.store.dispatch(new OpenSearch());
    this.searchOpen = true;
    this.searchFieldContent.nativeElement.focus();
    this.selectedOptions = this.selectionMemory.slice();
  }

  closeSearchList() {
    if (this.searchOpen) {
      this.store.dispatch(new CloseSearch());
      this.searchOpen = false;
    }
  }

  clearSearchField() {
    this.searchValue = '';
    this.filterStructuresOnSearch();
  }

  @HostListener('document:click', ['$event'])
  clickOutsideSearchList(event: MouseEvent) {
    const targetElement = event.target as HTMLElement;
    // Check if the click was outside the element
    if (targetElement && !this.elementRef.nativeElement.contains(targetElement)) {
      this.closeSearchList();
    }
  }

  // This method filters the structures on every letter typed
  public filterStructuresOnSearch() {
    if (!this.structures) {
      return;
    }
    if (!this.searchValue) {
      this.searchFilteredStructures = this.structures.slice();
      return;
    }
    // filter the structures
    this.searchFilteredStructures = this.structures.filter((structures) =>
      structures.name.toLowerCase().includes(this.searchValue.toLowerCase()),
    );
    // This event fires for every letter typed
    this.ga.event(GaAction.INPUT, GaCategory.NAVBAR, `Search term typed in: ${this.searchValue}`);
  }

  filterToggleChange(value: string[]) {
    this.ga.event(GaAction.TOGGLE, GaCategory.NAVBAR, `Structure Group Name Toggle: ${this.searchValue}`);

    if (value.length === 0) {
      this.groupFilteredStructures = this.structures.slice();
      return;
    }

    this.groupFilteredStructures = this.structures.filter((structure) => value.includes(structure.groupName));
  }

  // Hide a structure if it is absent from the filtered group list, otherwise hide when absent from the
  // filtered search list
  hideStructure(structure: SearchStructure) {
    return (
      this.groupFilteredStructures.indexOf(structure) <= -1 || this.searchFilteredStructures.indexOf(structure) <= -1
    );
  }
}
<div class="h-100 w-100 search-container">
  <div class="pl-2 br-left search-icon-container">
    <mat-icon class="mt-2">search</mat-icon>
  </div>
  <button
    mat-flat-button
    id="searchBtn"
    [disabled]="disabled"
    (click)="openSearchList()"
    class="w-100 secondary ch text-start"
    #tooltip="matTooltip"
    matTooltip="Search Structures"
    matTooltipPosition="below"
  >
    <span>{{ selectedValues }}</span>
    <mat-icon class="droprown-arrow-icon">arrow_drop_down</mat-icon>
  </button>
  <div class="search-modal mat-elevation-z4" [hidden]="(searchState$ | async) === false">
    <div class="search-controls br-top">
      <mat-form-field class="br-top" appearance="fill" floatLabel="auto">
        <input
          matInput
          cdkFocusInitial
          #searchField
          (keyup.enter)="selectFirstOption()"
          [(ngModel)]="searchValue"
          placeholder="Search Structures"
          (input)="filterStructuresOnSearch()"
        />
        <button
          matSuffix
          mat-icon-button
          #clearBtn
          [hidden]="!searchValue"
          aria-label="Clear Search"
          matTooltip="Clear Search"
          (click)="clearSearchField()"
        >
          <mat-icon>close</mat-icon>
        </button>
        <button matSuffix mat-icon-button aria-label="Close" matTooltip="Close" (click)="closeSearchList()">
          <mat-icon>arrow_drop_up</mat-icon>
        </button>
      </mat-form-field>
      <br />
      <p>
        <span id="label">Show<br />only:</span>
        <mat-button-toggle-group
          multiple
          #group="matButtonToggleGroup"
          (change)="filterToggleChange(group.value)"
          name="searchFilterButtons"
          aria-label="searchFilterButtons"
        >
          <mat-button-toggle value="Anatomical Structures" matTooltip="Anatomical Structures">AS</mat-button-toggle>
          <mat-button-toggle value="Cell Types" matTooltip="Cell Types">CT</mat-button-toggle>
          <mat-button-toggle value="Biomarkers" matTooltip="Biomarkers">B</mat-button-toggle>
        </mat-button-toggle-group>
        <button class="selectBtn" mat-stroked-button aria-label="Select All" (click)="selectAllOptions()">
          Select All
        </button>
        <button class="selectBtn" mat-stroked-button aria-label="Deselect All" (click)="deselectAllOptions()">
          Deselect All
        </button>
      </p>
    </div>
    <mat-selection-list
      #multiSelect
      [disabled]="disabled"
      [multiple]="true"
      (selectionChange)="selectOption()"
      [(ngModel)]="selectedOptions"
      [compareWith]="selectionCompareFunction"
    >
      <mat-list-item *ngIf="searchFilteredStructures.length === 0 && groupFilteredStructures.length === 0">
        <div>No entries found.</div>
      </mat-list-item>
      <mat-list-option
        *ngFor="let structure of structures"
        [value]="structure"
        color="primary"
        [hidden]="hideStructure(structure)"
        [selected]="isSelected(structure)"
        checkboxPosition="before"
      >
        <div class="structure-name">
          <div>
            {{ structure.name }}
          </div>
          <div class="structure-group-name">
            <em>{{ structure.groupName }}</em>
          </div>
        </div>
      </mat-list-option>
    </mat-selection-list>
  </div>
</div>

./search.component.scss

@use '@angular/material' as mat;
.mat-select-arrow {
  visibility: hidden;
}

.selection-icon {
  position: relative;
  top: 0.375rem;
  margin-right: 1rem;
}

.ch {
  height: 2.5812rem !important;
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}

.br-left {
  border-top-left-radius: 0.5rem !important;
  border-bottom-left-radius: 0.5rem !important;
}

.br-top {
  border-top-left-radius: 0.25rem !important;
  border-top-right-radius: 0.25rem !important;
}

.search-container {
  display: flex;
  align-items: center;
}
.search-icon-container {
  display: inline-block;
  background: #f5f5f5;
  padding-left: 0.6rem;
}
.structure-name {
  display: flex;
  justify-content: space-between;
  font-size: 11pt;
}
.structure-group-name {
  font-size: 8pt;
}
::ng-deep .search-container .mat-form-field-underline {
  display: none;
}

#searchBtn {
  mat-icon {
    float: right;
    position: absolute;
    top: 9px;
    right: 10px;
  }

  ::ng-deep .mat-button-wrapper {
    text-overflow: ellipsis;
    display: block;
    overflow: hidden;
    white-space: nowrap;
    padding-right: 10px;
  }
}

.search-modal {
  background: white;
  position: absolute;
  width: 375px;
  top: 10px;
  z-index: 20;
  border-bottom: 0.1875rem solid rgba(68, 74, 101, 0.063);
  border-radius: 0.25rem;

  .search-controls {
    position: fixed;
    z-index: 30;
    border-bottom: 1px lightgray solid;
    background: white;
    width: 375px;

    p {
      padding: 0px 15px;
      font-size: 10pt;
      display: flex;
      justify-content: center;
      align-content: center;
      margin-bottom: 10px;
    }

    #label {
      width: 35px;
      line-height: 14px;
      font-size: 8pt;
      display: inline-block;
      padding-top: 4px;
    }

    ::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
      line-height: 32px !important;
    }

    button.selectBtn {
      font-size: 13px;
      margin-left: 12px;
      padding: 0px 7px;
    }
  }

  mat-form-field {
    padding: 10px 15px;
    width: 100%;
    height: 55px;
    font-size: 12pt;
    //border-bottom: 1px rgb(238, 238, 238) solid;
    color: gray;

    button {
      font-size: 18pt;
      color: black;
    }

    input {
      color: rgba(0, 0, 0, 0.87);
    }
  }

  .mat-form-field-flex {
    padding-top: 0px;
  }

  mat-selection-list {
    margin-top: 95px;
    overflow-y: scroll;
    height: 300px;
  }

  mat-list-option {
    height: 40px;
    font-size: 11pt;
  }
}

.droprown-arrow-icon {
  transform: scale(1.4);
}
//@include mat.checkbox-theme($mat-reporter-primary);
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""