From 1febe9b536f6b688b472e4e8bf48d40ee46282f4 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Mon, 7 Oct 2019 17:47:40 +0200
Subject: [PATCH] chore: remove used var chore: allow extra layer dialog to be
 dismissed chore: added short cut o remove extra layers

---
 .../atlasViewer.constantService.service.ts    |  6 ++
 src/services/state/ngViewerState.store.ts     | 76 ++++++++++++++-
 src/ui/layerbrowser/layerbrowser.component.ts |  7 ++
 .../mobileOverlay/mobileOverlay.component.ts  |  8 +-
 .../nehubaContainer.component.ts              | 36 +++----
 .../nehubaContainer.template.html             |  4 +-
 .../searchSideNav/searchSideNav.component.ts  | 33 ++++++-
 .../searchSideNav/searchSideNav.template.html | 21 +++-
 .../signinBanner/signinBanner.components.ts   | 18 ++++
 .../signinBanner/signinBanner.template.html   |  3 +-
 src/ui/ui.module.ts                           |  2 -
 src/util/directives/help.directive.ts         | 30 ------
 .../directives/keyDownListener.directive.ts   | 96 +++++++++++++++++++
 .../mouseOver.directive.spec.ts               |  0
 .../{ => directives}/mouseOver.directive.ts   |  2 +-
 src/util/util.module.ts                       |  9 +-
 16 files changed, 278 insertions(+), 73 deletions(-)
 delete mode 100644 src/util/directives/help.directive.ts
 create mode 100644 src/util/directives/keyDownListener.directive.ts
 rename src/util/{ => directives}/mouseOver.directive.spec.ts (100%)
 rename src/util/{ => directives}/mouseOver.directive.ts (99%)

diff --git a/src/atlasViewer/atlasViewer.constantService.service.ts b/src/atlasViewer/atlasViewer.constantService.service.ts
index e0c159f04..548e57cca 100644
--- a/src/atlasViewer/atlasViewer.constantService.service.ts
+++ b/src/atlasViewer/atlasViewer.constantService.service.ts
@@ -238,10 +238,12 @@ Send us an email: <a target = "_blank" href = "mailto:${this.supportEmailAddress
           this.showHelpSliceViewMap = this.showHelpSliceViewMobile
           this.showHelpGeneralMap = this.showHelpGeneralMobile
           this.showHelpPerspectiveViewMap = this.showHelpPerspectiveMobile
+          this.dissmissUserLayerSnackbarMessage = this.dissmissUserLayerSnackbarMessageMobile
         } else {
           this.showHelpSliceViewMap = this.showHelpSliceViewDesktop
           this.showHelpGeneralMap = this.showHelpGeneralDesktop
           this.showHelpPerspectiveViewMap = this.showHelpPerspectiveDesktop
+          this.dissmissUserLayerSnackbarMessage = this.dissmissUserLayerSnackbarMessageDesktop
         }
       })
     )
@@ -263,6 +265,10 @@ Send us an email: <a target = "_blank" href = "mailto:${this.supportEmailAddress
   }
 
   public cyclePanelMessage: string = `[spacebar] to cycle through views`
+
+  private dissmissUserLayerSnackbarMessageDesktop = `You can dismiss extra layers with [ESC]` 
+  private dissmissUserLayerSnackbarMessageMobile = `You can dismiss extra layers in the 🌏 menu`
+  public dissmissUserLayerSnackbarMessage: string = this.dissmissUserLayerSnackbarMessageDesktop
 }
 
 const parseURLToElement = (url:string):HTMLElement=>{
diff --git a/src/services/state/ngViewerState.store.ts b/src/services/state/ngViewerState.store.ts
index c3f24dcc1..3467d2258 100644
--- a/src/services/state/ngViewerState.store.ts
+++ b/src/services/state/ngViewerState.store.ts
@@ -2,9 +2,10 @@ import { Action, Store, select } from '@ngrx/store'
 import { Injectable, OnDestroy } from '@angular/core';
 import { Observable, combineLatest, fromEvent, Subscription } from 'rxjs';
 import { Effect, Actions, ofType } from '@ngrx/effects';
-import { withLatestFrom, map, distinctUntilChanged, scan, shareReplay, filter, mapTo, tap, delay } from 'rxjs/operators';
+import { withLatestFrom, map, distinctUntilChanged, scan, shareReplay, filter, mapTo, tap, delay, switchMapTo, take } from 'rxjs/operators';
 import { AtlasViewerConstantsServices } from 'src/atlasViewer/atlasViewer.constantService.service';
 import { SNACKBAR_MESSAGE } from './uiState.store';
+import { getNgIds } from '../stateStore.service';
 
 export const FOUR_PANEL = 'FOUR_PANEL'
 export const V_ONE_THREE = 'V_ONE_THREE'
@@ -24,6 +25,7 @@ export interface NgViewerStateInterface{
 
 export interface NgViewerAction extends Action{
   layer : NgLayerInterface
+  layers : NgLayerInterface[]
   forceShowSegment : boolean
   nehubaReady: boolean
   payload: any
@@ -82,6 +84,13 @@ export function ngViewerState(prevState:NgViewerStateInterface = defaultState, a
                   : {})
           })
       } 
+    case REMOVE_NG_LAYERS:
+      const { layers } = action
+      const layerNameSet = new Set(layers.map(l => l.name))
+      return {
+        ...prevState,
+        layers: prevState.layers.filter(l => !layerNameSet.has(l.name))
+      }
     case REMOVE_NG_LAYER:
       return {
         ...prevState,
@@ -140,6 +149,9 @@ export class NgViewerUseEffect implements OnDestroy{
   @Effect()
   public spacebarListener$: Observable<any>
 
+  @Effect()
+  public removeAllNonBaseLayers$: Observable<any>
+
   private panelOrder$: Observable<string>
   private panelMode$: Observable<string>
 
@@ -281,6 +293,63 @@ export class NgViewerUseEffect implements OnDestroy{
         type: ACTION_TYPES.CYCLE_VIEWS
       })
     )
+
+    /**
+     * simplify with layer browser
+     */
+    const baseNgLayerName$ = this.store$.pipe(
+      select('viewerState'),
+      select('templateSelected'),
+      map(templateSelected => {
+        if (!templateSelected) return []
+
+        const { ngId , otherNgIds = []} = templateSelected
+
+        return [
+          ngId,
+          ...otherNgIds,
+          ...templateSelected.parcellations.reduce((acc, curr) => {
+            return acc.concat([
+              curr.ngId,
+              ...getNgIds(curr.regions)
+            ])
+          }, [])
+        ]
+      }),
+      /**
+       * get unique array
+       */
+      map(nonUniqueArray => Array.from(new Set(nonUniqueArray))),
+      /**
+       * remove falsy values
+       */
+      map(arr => arr.filter(v => !!v))
+    )
+
+    const allLoadedNgLayers$ = this.store$.pipe(
+      select('viewerState'),
+      select('loadedNgLayers')
+    )
+
+    this.removeAllNonBaseLayers$ = this.actions.pipe(
+      ofType(ACTION_TYPES.REMOVE_ALL_NONBASE_LAYERS),
+      withLatestFrom(
+        combineLatest(
+          baseNgLayerName$,
+          allLoadedNgLayers$
+        )
+      ),
+      map(([_, [baseNgLayerNames, loadedNgLayers] ]) => {
+        const baseNameSet = new Set(baseNgLayerNames)
+        return loadedNgLayers.filter(l => !baseNameSet.has(l.name))
+      }),
+      map(layers => {
+        return {
+          type: REMOVE_NG_LAYERS,
+          layers
+        }
+      })
+    )
   }
 
   ngOnDestroy(){
@@ -292,6 +361,7 @@ export class NgViewerUseEffect implements OnDestroy{
 
 export const ADD_NG_LAYER = 'ADD_NG_LAYER'
 export const REMOVE_NG_LAYER = 'REMOVE_NG_LAYER'
+export const REMOVE_NG_LAYERS = 'REMOVE_NG_LAYERS'
 export const SHOW_NG_LAYER = 'SHOW_NG_LAYER'
 export const HIDE_NG_LAYER = 'HIDE_NG_LAYER'
 export const FORCE_SHOW_SEGMENT = `FORCE_SHOW_SEGMENT`
@@ -311,7 +381,9 @@ const ACTION_TYPES = {
   SET_PANEL_ORDER: 'SET_PANEL_ORDER',
 
   TOGGLE_MAXIMISE: 'TOGGLE_MAXIMISE',
-  CYCLE_VIEWS: 'CYCLE_VIEWS'
+  CYCLE_VIEWS: 'CYCLE_VIEWS',
+
+  REMOVE_ALL_NONBASE_LAYERS: `REMOVE_ALL_NONBASE_LAYERS`
 }
 
 export const SUPPORTED_PANEL_MODES = [
diff --git a/src/ui/layerbrowser/layerbrowser.component.ts b/src/ui/layerbrowser/layerbrowser.component.ts
index 7fefc4b16..12cba26e2 100644
--- a/src/ui/layerbrowser/layerbrowser.component.ts
+++ b/src/ui/layerbrowser/layerbrowser.component.ts
@@ -5,6 +5,7 @@ import { ViewerStateInterface, isDefined, REMOVE_NG_LAYER, FORCE_SHOW_SEGMENT, s
 import { Subscription, Observable, combineLatest } from "rxjs";
 import { filter, map, shareReplay, distinctUntilChanged, throttleTime, debounceTime } from "rxjs/operators";
 import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
+import { NG_VIEWER_ACTION_TYPES } from "src/services/state/ngViewerState.store";
 
 @Component({
   selector : 'layer-browser',
@@ -181,6 +182,12 @@ export class LayerBrowser implements OnInit, OnDestroy{
     })
   }
 
+  removeAllNonBasicLayer(){
+    this.store.dispatch({
+      type: NG_VIEWER_ACTION_TYPES.REMOVE_ALL_NONBASE_LAYERS
+    })
+  }
+
   removeLayer(layer:any){
     if(this.checkLocked(layer)){
       console.warn('this layer is locked and cannot be removed')
diff --git a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts
index 9be2dc782..9971a1b4b 100644
--- a/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts
+++ b/src/ui/nehubaContainer/mobileOverlay/mobileOverlay.component.ts
@@ -62,14 +62,18 @@ export class MobileOverlay implements OnInit, OnDestroy{
   }
 
   ngOnInit(){
+
+    const itemCount = this.tunableProperties.length
+
     const config = {
       root: this.intersector.nativeElement,
-      threshold: [...Array(10)].map((_, k) => k / 10)
+      threshold: [...[...Array(itemCount)].map((_, k) => k / itemCount), 1]
     }
 
     this.intersectionObserver = new IntersectionObserver((arg) => {
       if (arg[0].isIntersecting) {
-        this.focusItemIndex = 2- Math.floor(arg[0].intersectionRatio * this.tunableProperties.length)
+        const ratio = arg[0].intersectionRatio - (1 / this.tunableProperties.length / 2)
+        this.focusItemIndex = this.tunableProperties.length - Math.round(ratio * this.tunableProperties.length) - 1
       }
     }, config)
 
diff --git a/src/ui/nehubaContainer/nehubaContainer.component.ts b/src/ui/nehubaContainer/nehubaContainer.component.ts
index 35899d499..4d941cdbb 100644
--- a/src/ui/nehubaContainer/nehubaContainer.component.ts
+++ b/src/ui/nehubaContainer/nehubaContainer.component.ts
@@ -879,21 +879,25 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{
     })
   }
 
-  public tunableMobileProperties = ['Oblique Rotate X', 'Oblique Rotate Y', 'Oblique Rotate Z']
+  public tunableMobileProperties = ['Oblique Rotate X', 'Oblique Rotate Y', 'Oblique Rotate Z', 'Remove extra layers']
   public selectedProp = null
 
+  handleMobileOverlayTouchEnd(focusItemIndex){
+    if (this.tunableMobileProperties[focusItemIndex] === 'Remove extra layers') {
+      this.store.dispatch({
+        type: NG_VIEWER_ACTION_TYPES.REMOVE_ALL_NONBASE_LAYERS
+      })
+    }
+  }
+
   handleMobileOverlayEvent(obj:any){
     const {delta, selectedProp} = obj
     this.selectedProp = selectedProp
 
     const idx = this.tunableMobileProperties.findIndex(p => p === selectedProp)
-    idx === 0
-      ? this.nehubaViewer.obliqueRotateX(delta)
-      : idx === 1
-        ? this.nehubaViewer.obliqueRotateY(delta)
-        : idx === 2
-          ? this.nehubaViewer.obliqueRotateZ(delta)
-          : console.warn('could not oblique rotate')
+    if (idx === 0) this.nehubaViewer.obliqueRotateX(delta)
+    if (idx === 1) this.nehubaViewer.obliqueRotateY(delta)
+    if (idx === 2) this.nehubaViewer.obliqueRotateZ(delta)
   }
 
   returnTruePos(quadrant:number,data:any){
@@ -1313,22 +1317,6 @@ export class NehubaContainer implements OnInit, OnChanges, OnDestroy{
       }))
     }
   }
-
-  removeFav(event: MouseEvent, ds: DataEntry){
-    this.store.dispatch({
-      type: DATASETS_ACTIONS_TYPES.UNFAV_DATASET,
-      payload: ds
-    })
-  }
-
-  downloadDs(event: MouseEvent, ds: DataEntry, downloadBtn: MatButton){
-    downloadBtn.disabled = true
-    const id = getIdFromDataEntry(ds)
-    const { name } = ds
-    this.kgSingleDataset.downloadZipFromKg({kgId: id}, name)
-      .catch(err => this.constantService.catchError(err))
-      .finally(() => downloadBtn.disabled = false)
-  }
 }
 
 export const identifySrcElement = (element:HTMLElement) => {
diff --git a/src/ui/nehubaContainer/nehubaContainer.template.html b/src/ui/nehubaContainer/nehubaContainer.template.html
index d2c23e128..9d7cc3ea2 100644
--- a/src/ui/nehubaContainer/nehubaContainer.template.html
+++ b/src/ui/nehubaContainer/nehubaContainer.template.html
@@ -54,8 +54,10 @@
 <!-- mobile nub, allowing for ooblique slicing in mobile -->
 <mobile-overlay
   *ngIf="(useMobileUI$ | async) && viewerLoaded"
+  (touchend)="handleMobileOverlayTouchEnd(mobileOverlayEl.focusItemIndex)"
   [tunableProperties]="tunableMobileProperties"
-  (deltaValue)="handleMobileOverlayEvent($event)">
+  (deltaValue)="handleMobileOverlayEvent($event)"
+  #mobileOverlayEl>
   <div class="base" delta>
     <div mobileObliqueGuide class="p-2 mb-4 shadow">
       {{ selectedProp }}
diff --git a/src/ui/searchSideNav/searchSideNav.component.ts b/src/ui/searchSideNav/searchSideNav.component.ts
index fe072947e..c7a42be3b 100644
--- a/src/ui/searchSideNav/searchSideNav.component.ts
+++ b/src/ui/searchSideNav/searchSideNav.component.ts
@@ -1,5 +1,5 @@
-import { Component, Output, EventEmitter, OnInit, OnDestroy } from "@angular/core";
-import { MatDialogRef, MatDialog } from "@angular/material";
+import { Component, Output, EventEmitter, OnInit, OnDestroy, ViewChild, TemplateRef } from "@angular/core";
+import { MatDialogRef, MatDialog, MatSnackBar } from "@angular/material";
 import { NgLayerInterface } from "src/atlasViewer/atlasViewer.component";
 import { LayerBrowser } from "../layerbrowser/layerbrowser.component";
 import { Observable, Subscription } from "rxjs";
@@ -7,6 +7,7 @@ import { Store, select } from "@ngrx/store";
 import { map, startWith, scan, filter, mapTo } from "rxjs/operators";
 import { VIEWERSTATE_CONTROLLER_ACTION_TYPES } from "../viewerStateController/viewerState.base";
 import { trackRegionBy } from '../viewerStateController/regionHierachy/regionHierarchy.component'
+import { AtlasViewerConstantsServices } from "src/atlasViewer/atlasViewer.constantService.service";
 
 @Component({
   selector: 'search-side-nav',
@@ -26,11 +27,15 @@ export class SearchSideNav implements OnInit, OnDestroy {
   @Output() dismiss: EventEmitter<any> = new EventEmitter()
   @Output() open: EventEmitter<any> = new EventEmitter()
 
+  @ViewChild('layerBrowserTmpl', {read: TemplateRef}) layerBrowserTmpl: TemplateRef<any>
+
   public autoOpenSideNav$: Observable<any>
 
   constructor(
-    private dialog: MatDialog,
-    private store$: Store<any>
+    public dialog: MatDialog,
+    private store$: Store<any>,
+    private snackBar: MatSnackBar,
+    private constantService: AtlasViewerConstantsServices
   ){
     this.autoOpenSideNav$ = this.store$.pipe(
       select('viewerState'),
@@ -67,7 +72,9 @@ export class SearchSideNav implements OnInit, OnDestroy {
     if (this.layerBrowserDialogRef) return
     
     this.dismiss.emit(true)
-    this.layerBrowserDialogRef = this.dialog.open(LayerBrowser, {
+    
+    const dialogToOpen = this.layerBrowserTmpl || LayerBrowser
+    this.layerBrowserDialogRef = this.dialog.open(dialogToOpen, {
       hasBackdrop: false,
       autoFocus: false,
       position: {
@@ -75,6 +82,16 @@ export class SearchSideNav implements OnInit, OnDestroy {
       },
       disableClose: true
     })
+
+    this.layerBrowserDialogRef.afterClosed().subscribe(val => {
+      if (val === 'user action') this.snackBar.open(this.constantService.dissmissUserLayerSnackbarMessage, 'Dismiss', {
+        duration: 5000
+      })
+    })
+  }
+
+  extraLayersCanBeDismissed(): boolean{
+    return this.dialog.openDialogs.findIndex(dialog => dialog !== this.layerBrowserDialogRef) < 0
   }
 
   removeRegion(region: any){
@@ -85,4 +102,10 @@ export class SearchSideNav implements OnInit, OnDestroy {
   }
 
   trackByFn = trackRegionBy
+
+  public keyListenerConfig = [{
+    key: 'Escape',
+    type: 'keydown',
+    target: 'document'
+  }]
 }
\ No newline at end of file
diff --git a/src/ui/searchSideNav/searchSideNav.template.html b/src/ui/searchSideNav/searchSideNav.template.html
index 81f80bacd..b96c5d787 100644
--- a/src/ui/searchSideNav/searchSideNav.template.html
+++ b/src/ui/searchSideNav/searchSideNav.template.html
@@ -106,6 +106,23 @@
 </div>
 
 <div [hidden]>
-  <layer-browser (nonBaseLayersChanged)="handleNonbaseLayerEvent($event)" #layerBrowser>
+  <layer-browser
+    [iav-key-listener]="(layerBrowser.nonBaseNgLayers$ | async).length > 0 ? keyListenerConfig : null"
+    (iav-key-event)="extraLayersCanBeDismissed() ? layerBrowser.removeAllNonBasicLayer() : null"
+    (nonBaseLayersChanged)="handleNonbaseLayerEvent($event)"
+    #layerBrowser>
   </layer-browser>
-</div>
\ No newline at end of file
+</div>
+
+<ng-template #layerBrowserTmpl>
+  <mat-dialog-content>
+    <layer-browser></layer-browser>
+  </mat-dialog-content>
+  <mat-dialog-actions class="justify-content-end">
+    <button matTooltip="Rmove layers with [Esc]"
+      mat-button
+      mat-dialog-close="user action">
+      Hide
+    </button>
+  </mat-dialog-actions>
+</ng-template>
\ No newline at end of file
diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts
index 809daa23f..ac85379b6 100644
--- a/src/ui/signinBanner/signinBanner.components.ts
+++ b/src/ui/signinBanner/signinBanner.components.ts
@@ -45,4 +45,22 @@ export class SigninBanner{
       panelClass: ['col-12','col-sm-12','col-md-8','col-lg-6','col-xl-4']
     })
   }
+
+  private keyListenerConfigBase = {
+    type: 'keydown',
+    stop: true,
+    prevent: true,
+    target: 'document'
+  }
+
+  public keyListenerConfig = [{
+    key: 'h',
+    ...this.keyListenerConfigBase
+  },{
+    key: 'H',
+    ...this.keyListenerConfigBase
+  },{
+    key: '?',
+    ...this.keyListenerConfigBase
+  }]
 }
\ No newline at end of file
diff --git a/src/ui/signinBanner/signinBanner.template.html b/src/ui/signinBanner/signinBanner.template.html
index ec4762e48..8280808f3 100644
--- a/src/ui/signinBanner/signinBanner.template.html
+++ b/src/ui/signinBanner/signinBanner.template.html
@@ -1,5 +1,6 @@
 <div class="d-flex"
-  (helpdirective)="openTmplWithDialog(helpComponent)">
+  [iav-key-listener]="keyListenerConfig"
+  (iav-key-event)="openTmplWithDialog(helpComponent)">
 
   <!-- signin -->
   <div class="btnWrapper">
diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts
index d7dbd2d39..8aab1e839 100644
--- a/src/ui/ui.module.ts
+++ b/src/ui/ui.module.ts
@@ -71,7 +71,6 @@ import { RegionHierarchy } from './viewerStateController/regionHierachy/regionHi
 import { CurrentlySelectedRegions } from './viewerStateController/regionsListView/currentlySelectedRegions/currentlySelectedRegions.component'
 import { RegionTextSearchAutocomplete } from "./viewerStateController/regionSearch/regionSearch.component";
 import { RegionsListView } from "./viewerStateController/regionsListView/simpleRegionsListView/regionListView.component";
-import { HelpDirective } from "src/util/directives/help.directive";
 
 @NgModule({
   imports : [
@@ -149,7 +148,6 @@ import { HelpDirective } from "src/util/directives/help.directive";
     DownloadDirective,
     TouchSideClass,
     ElementOutClickDirective,
-    HelpDirective
   ],
   entryComponents : [
 
diff --git a/src/util/directives/help.directive.ts b/src/util/directives/help.directive.ts
deleted file mode 100644
index e6fd2a921..000000000
--- a/src/util/directives/help.directive.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Directive, HostListener, Output, EventEmitter} from '@angular/core'
-
-const HELP_SYMBOL = Symbol('HELP_SYMBOL')
-
-@Directive({
-  selector : '[helpdirective]'
-})
-export class HelpDirective{
-
-  @Output('helpdirective')
-  callhelp: EventEmitter<KeyboardEvent> = new EventEmitter()
-
-  @HostListener('document:keydown', ['$event'])
-  keydownHandler(ev:KeyboardEvent){
-    
-    const target = <HTMLElement> ev.target
-    const tagName = target.tagName
-
-    if (tagName === 'SELECT' || tagName === 'INPUT' || tagName === 'TEXTAREA') return
-    
-    if (ev.key === 'h' || ev.key === 'H' || ev.key === '?') {
-      ev.stopPropagation()
-      ev.preventDefault()
-      /**
-       * call help modal
-       */
-      this.callhelp.emit(ev)
-    }
-  }
-}
\ No newline at end of file
diff --git a/src/util/directives/keyDownListener.directive.ts b/src/util/directives/keyDownListener.directive.ts
new file mode 100644
index 000000000..b6423f210
--- /dev/null
+++ b/src/util/directives/keyDownListener.directive.ts
@@ -0,0 +1,96 @@
+import { Directive, Input, HostListener, Output, EventEmitter } from "@angular/core";
+
+const getFilterFn = (ev: KeyboardEvent, isDocument: boolean) => ({ type, key, target }: KeyListenerConfig): boolean => type === ev.type && ev.key === key && (target === 'document') === isDocument
+
+@Directive({
+  selector: '[iav-key-listener]'
+})
+
+export class KeyListner{
+
+  @Input('iav-key-listener')
+  keydownConfig: KeyListenerConfig[] = []
+
+  private isTextField(ev: KeyboardEvent):boolean{
+
+    const target = <HTMLElement> ev.target
+    const tagName = target.tagName
+
+    return (tagName === 'SELECT' || tagName === 'INPUT' || tagName === 'TEXTAREA') 
+  }
+
+  @HostListener('keydown', ['$event'])
+  keydown(ev: KeyboardEvent){
+    this.handleSelfListener(ev)
+  }
+
+  @HostListener('document:keydown', ['$event'])
+  documentKeydown(ev: KeyboardEvent){
+    this.handleDocumentListener(ev)
+  }
+
+  @HostListener('keyup', ['$event'])
+  keyup(ev: KeyboardEvent){
+    this.handleSelfListener(ev)
+  }
+
+  @HostListener('document:keyup', ['$event'])
+  documentKeyup(ev: KeyboardEvent){
+    this.handleDocumentListener(ev)
+  }
+
+  private handleSelfListener(ev: KeyboardEvent) {
+    if (!this.keydownConfig) return
+    if (this.isTextField(ev)) return
+
+    const filteredConfig = this.keydownConfig
+      .filter(getFilterFn(ev, false))
+      .map(config => {
+        return {
+          config,
+          ev
+        }
+      })
+    this.emitEv(filteredConfig)
+  }
+
+  private handleDocumentListener(ev:KeyboardEvent) {
+    if (!this.keydownConfig) return
+    if (this.isTextField(ev)) return
+
+    const filteredConfig = this.keydownConfig
+      .filter(getFilterFn(ev, true))
+      .map(config => {
+        return {
+          config,
+          ev
+        }
+      })
+    this.emitEv(filteredConfig)
+  }
+
+  private emitEv(items: {config:KeyListenerConfig, ev: KeyboardEvent}[]){
+    for (const item of items){
+      const { config, ev } = item as {config:KeyListenerConfig, ev: KeyboardEvent}
+
+      const { stop, prevent } = config
+      if (stop) ev.stopPropagation()
+      if (prevent) ev.preventDefault()
+
+      this.keyEvent.emit({
+        config, ev
+      })
+    }
+  }
+
+  @Output('iav-key-event') keyEvent = new EventEmitter<{ config: KeyListenerConfig, ev: KeyboardEvent }>()
+
+}
+
+export interface KeyListenerConfig{
+  type: 'keydown' | 'keyup'
+  key: string
+  target?: 'document'
+  stop: boolean
+  prevent: boolean
+}
diff --git a/src/util/mouseOver.directive.spec.ts b/src/util/directives/mouseOver.directive.spec.ts
similarity index 100%
rename from src/util/mouseOver.directive.spec.ts
rename to src/util/directives/mouseOver.directive.spec.ts
diff --git a/src/util/mouseOver.directive.ts b/src/util/directives/mouseOver.directive.ts
similarity index 99%
rename from src/util/mouseOver.directive.ts
rename to src/util/directives/mouseOver.directive.ts
index 46333a540..24ca7ddee 100644
--- a/src/util/mouseOver.directive.ts
+++ b/src/util/directives/mouseOver.directive.ts
@@ -204,4 +204,4 @@ export class MouseOverIconPipe implements PipeTransform{
         }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/util/util.module.ts b/src/util/util.module.ts
index 239488050..229608e35 100644
--- a/src/util/util.module.ts
+++ b/src/util/util.module.ts
@@ -3,7 +3,8 @@ import { FilterNullPipe } from "./pipes/filterNull.pipe";
 import { FilterRowsByVisbilityPipe } from "src/components/flatTree/filterRowsByVisibility.pipe";
 import { StopPropagationDirective } from "./directives/stopPropagation.directive";
 import { DelayEventDirective } from "./directives/delayEvent.directive";
-import { MouseHoverDirective, MouseOverTextPipe, MouseOverIconPipe } from "./mouseOver.directive";
+import { MouseHoverDirective, MouseOverTextPipe, MouseOverIconPipe } from "./directives/mouseOver.directive";
+import { KeyListner } from "./directives/keyDownListener.directive";
 
 @NgModule({
   declarations: [
@@ -13,7 +14,8 @@ import { MouseHoverDirective, MouseOverTextPipe, MouseOverIconPipe } from "./mou
     DelayEventDirective,
     MouseHoverDirective,
     MouseOverTextPipe,
-    MouseOverIconPipe
+    MouseOverIconPipe,
+    KeyListner
   ],
   exports: [
     FilterNullPipe,
@@ -22,7 +24,8 @@ import { MouseHoverDirective, MouseOverTextPipe, MouseOverIconPipe } from "./mou
     DelayEventDirective,
     MouseHoverDirective,
     MouseOverTextPipe,
-    MouseOverIconPipe
+    MouseOverIconPipe,
+    KeyListner
   ]
 })
 
-- 
GitLab