import { Directive , Input , OnInit , QueryList , ViewChild } from '@angular/core' ; import { fromEvent } from 'rxjs' ; import { filter } from 'rxjs/operators' ; export type FeedScrollDirection = 'up' | 'down' | '' ; export type AnchorElement = { element : any ; position : number ; } ; export const TOPBAR_HEIGHT = 75 ; ViewChildren @ Directive ( { selector : '[m-hotkeyScroll]' , } ) export class HotkeyScrollDirective implements OnInit { @ Input ( ) anchors : QueryList < ViewChild > ; private readonly keyPresses = fromEvent ( document , 'keydown' ) . pipe ( filter ( ( e ) => { if ( this . isFocusCaptured ( e . target ) ) { return ; } return e . key === 'j' || e . key === 'J' || e . key === 'k' || e . key === 'K' ; } ) ) ; ngOnInit ( ) { this . keyPresses . subscribe ( val => { if ( ! this . anchors ) { return ; } let arr = [ ] ; let direction = this . getDirection ( val . key ) ; this . anchors . forEach ( ( anchor ) => { if ( ! anchor || ! anchor . nativeElement ) { return ; } arr . push ( { element : anchor , position : anchor . nativeElement . getBoundingClientRect ( ) . top , } ) ; } ) ; const currentPosition = this . getCurrentPosition ( arr ) ; const feedArray = this . anchors . toArray ( ) ; const currentElArr = arr . filter ( val => val . position === currentPosition ) ; let currentEl ; if ( currentElArr . length > 1 ) { currentEl = direction === 'down' ? currentElArr . reverse ( ) [ 0 ] : currentElArr [ 0 ] ; } else { currentEl = currentElArr [ 0 ] ; } if ( ! currentEl ) { return ; } const currentIndex = feedArray . indexOf ( currentEl . element ) ; const nextIndex = direction === 'down' ? currentIndex + 1 : currentIndex - 1 ; const nextElement = ( feedArray [ nextIndex ] as any ) ?. nativeElement ; if ( ! nextElement ) { return ; } const nextPosition = ( nextElement ?. getBoundingClientRect ( ) . top ?? 0 ) + ( window . pageYOffset - TOPBAR_HEIGHT ) ; this . scrollToPosition ( nextPosition ) ; } ) ; } param { string } key returns { FeedScrollDirection } private getDirection ( key ) { if ( key === 'j' || key === 'J' ) { return 'up' ; } else { return 'down' ; } } param { AnchorElement [ ] } arr returns private getCurrentPosition ( arr ) { return arr . reduce ( ( acc , { position } ) => acc === 0 ? position : position > 0 && position <= Math . abs ( acc ) ? position : position < 0 && - position < Math . abs ( acc ) ? position : acc , 0 ) ; } param { number } position returns { void } private scrollToPosition ( position ) { window . scrollTo ( { top : position , behavior : 'smooth' , } ) ; } param target returns { boolean } private isFocusCaptured ( target ) { try { return ( target . localName === 'input' || target . localName === 'textarea' || target . isContentEditable ) ; } catch ( e ) { return true ; } } }