import { DOWN_ARROW , LEFT_ARROW , RIGHT_ARROW , UP_ARROW } from '@angular/cdk/keycodes' ; import { Injectable , OnDestroy } from '@angular/core' ; import { BehaviorSubject , Subject } from 'rxjs' ; import { SankeyNodeLink } from './interfaces/node-link.interface' ; @ Injectable ( ) export class SankeyFocusManager < T > implements OnDestroy { /** Store the node that can currently be tabbed to */ active$ = new BehaviorSubject < SankeyNodeLink < T > > ( null ) ; /** Emit whenever an item should receive focus */ focused$ = new Subject < SankeyNodeLink < T > > ( ) ; /** Store the nodes  */ private _nodes : ReadonlyArray < SankeyNodeLink < T > > = [ ] ; /** Get the current active item */ private get _active ( ) : SankeyNodeLink < T > { return this . active$ . value ; } ngOnDestroy ( ) { this . active$ . complete ( ) ; this . focused$ . complete ( ) ; } /** Update the list of possible nodes */ setNodes ( nodes ) { this . _nodes = nodes ; if ( ! this . hasActiveNode ( ) ) { this . setActiveItem ( this . _nodes [ 0 ] ) ; } } /** Set the current active item */ setActiveItem ( node ) { this . active$ . next ( node ) ; } /** Handle keyboard input from nodes */ onKeydown ( event ) { switch ( event . which ) { case UP_ARROW : this . shiftFocusVertically ( - 1 ) ; event . preventDefault ( ) ; break ; case DOWN_ARROW : this . shiftFocusVertically ( 1 ) ; event . preventDefault ( ) ; break ; case LEFT_ARROW : this . shiftFocusHorizontally ( - 1 ) ; event . preventDefault ( ) ; break ; case RIGHT_ARROW : this . shiftFocusHorizontally ( 1 ) ; event . preventDefault ( ) ; break ; } } private setFocusedItem ( item ) { this . setActiveItem ( item ) ; this . focused$ . next ( item ) ; } private shiftFocusVertically ( delta ) { const nodes = this . getNodesInColumn ( this . _active . column ) ; const target = nodes [ nodes . findIndex ( node => node . node . id === this . _active . node . id ) + delta ] ; if ( target ) { this . setFocusedItem ( target ) ; } } /** Shift the focus to a node in a sibling column */ private shiftFocusHorizontally ( delta ) { const nodes = this . getNodesInColumn ( this . _active . column + delta ) ; if ( nodes . length === 0 ) { return ; } const target = nodes . reduce ( ( closest , node ) => { const closestDiff = Math . max ( closest . y , this . _active . y ) - Math . min ( closest . y , this . _active . y ) ; const currentDiff = Math . max ( node . y , this . _active . y ) - Math . min ( node . y , this . _active . y ) ; return closestDiff < currentDiff ? closest : node ; } ) ; if ( target ) { this . setFocusedItem ( target ) ; } } /** Get a list of nodes that are in a given column */ private getNodesInColumn ( column ) { return this . getNodesInOrder ( this . _nodes . filter ( node => node . column === column ) ) ; } /** Sort the nodes based on the Y position */ private getNodesInOrder ( nodes ) { return [ ... nodes ] . sort ( ( nodeOne , nodeTwo ) => nodeOne . y - nodeTwo . y ) ; } /** Determine whether or not there is a not that is tabbable */ private hasActiveNode ( ) { return ! ! this . active$ . value && ! ! this . _nodes . find ( node => node . node . id === this . active$ . value . node . id ) ; } }