import { isPlatformBrowser } from '@angular/common' ; import { AfterViewInit , ContentChildren , Directive , ElementRef , EventEmitter , HostListener , Inject , OnDestroy , Output , PLATFORM_ID , QueryList , Renderer2 } from '@angular/core' ; import { SplitAreaDirective , SplitComponent } from 'angular-split' ; import { Subject } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { FocusIndicator } from '../focus-indicator/focus-indicator' ; import { FocusIndicatorService } from '../focus-indicator/focus-indicator.service' ; @ Directive ( { selector : 'as-split' } ) export class SplitterAccessibilityDirective implements AfterViewInit , OnDestroy { /** Emit an event whenever the gutter is moved using the keyboard */ @ Output ( ) gutterKeydown = new EventEmitter < KeyboardEvent > ( ) ; /** Find all the split areas */ @ ContentChildren ( SplitAreaDirective ) areas : QueryList < SplitAreaDirective > ; /** Store all the gutter elements */ private _gutters : HTMLElement [ ] = [ ] ; /** Watch for gutters being added or removed */ private _observer : MutationObserver ; /** Teardown our observables on destroy */ private _onDestroy = new Subject < void > ( ) ; /** Store references to all focus indicators */ private _focusIndicators : FocusIndicator [ ] = [ ] ; constructor ( private _elementRef , private _renderer , @ Inject ( PLATFORM_ID ) private _platform , private _splitter , private _focusIndicatorService ) { _splitter . dragProgress$ . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . updateGutterAttributes ( ) ) ; } /** Once initialised make the gutters accessible */ ngAfterViewInit ( ) { this . onGutterChange ( ) ; this . areas . changes . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . onGutterChange ( ) ) ; if ( isPlatformBrowser ( this . _platform ) ) { this . _observer = new MutationObserver ( ( ) => this . onGutterChange ( ) ) ; this . _observer . observe ( this . _elementRef . nativeElement , { childList : true } ) ; } } /** Destroy all observables and observers */ ngOnDestroy ( ) { if ( this . _observer ) { this . _observer . disconnect ( ) ; } this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; this . _focusIndicators . forEach ( indicator => indicator . destroy ( ) ) ; } /** We should focus the gutter when it is clicked */ @ HostListener ( 'click' , [ '$event' ] ) onClick ( event ) { if ( this . isSplitterGutter ( event . target as HTMLElement ) ) { ( event . target as HTMLInputElement ) . parentElement . focus ( ) ; } } /** Find all the gutters and set their attributes */ private onGutterChange ( ) { this . _focusIndicators . forEach ( indicator => indicator . destroy ( ) ) ; this . _focusIndicators = [ ] ; this . _gutters = this . getGutters ( ) ; this . _gutters . forEach ( gutter => this . _focusIndicators . push ( this . _focusIndicatorService . monitor ( gutter ) ) ) ; this . setGutterAttributes ( ) ; } /** Get all the gutter elements */ private getGutters ( ) { if ( isPlatformBrowser ( this . _platform ) ) { const gutters = [ ] ; for ( let idx = 0 ; idx < this . _elementRef . nativeElement . children . length ; idx ++ ) { const node = this . _elementRef . nativeElement . children . item ( idx ) ; if ( this . isSplitterGutter ( node as HTMLElement ) ) { gutters . push ( node as HTMLElement ) ; } } return gutters ; } return [ ] ; } /** Set the appropriate attributes on the gutter elements */ private setGutterAttributes ( ) { this . _gutters . forEach ( gutter => { this . _renderer . setAttribute ( gutter , 'role' , 'separator' ) ; this . _renderer . setAttribute ( gutter , 'tabindex' , '0' ) ; this . updateGutterAttributes ( ) ; } ) ; } /** Apply the aria attribute values */ private updateGutterAttributes ( ) { this . _gutters . forEach ( ( gutter , idx ) => { this . setGutterValueNow ( gutter , idx ) ; this . setGutterValueMin ( gutter , idx ) ; this . setGutterValueMax ( gutter , idx ) ; } ) ; } /** Apply the value now aria attribute */ private setGutterValueNow ( gutter , index ) { const area = this . _splitter . displayedAreas [ index ] ; this . _renderer . setAttribute ( gutter , 'aria-valuenow' , `${ Math . round ( area . size ) }` ) ; } /** Apply the value min aria attribute */ private setGutterValueMin ( gutter , index ) { const area = this . areas . toArray ( ) [ index ] ; this . _renderer . setAttribute ( gutter , 'aria-valuemin' , `${ Math . round ( area . minSize ) }` ) ; } /** Apply the value max aria attribute */ private setGutterValueMax ( gutter , index ) { const availableSize = this . areas . filter ( ( _area , idx ) => index !== idx ) . reduce < number > ( ( total , area ) => total + area . minSize , 0 ) ; this . _renderer . setAttribute ( gutter , 'aria-valuemax' , `${ 100 - Math . round ( availableSize ) }` ) ; } @ HostListener ( 'keydown' , [ '$event' ] ) onKeydown ( event ) { if ( this . isSplitterGutter ( event . target as HTMLElement ) ) { this . gutterKeydown . emit ( event ) ; } } @ HostListener ( 'keydown.ArrowDown' , [ '$event' ] ) @ HostListener ( 'keydown.ArrowRight' , [ '$event' ] ) onIncreaseKey ( event ) { if ( this . isSplitterGutter ( event . target as HTMLElement ) ) { this . setGutterPosition ( event . target as HTMLElement , - 1 ) ; event . preventDefault ( ) ; } } @ HostListener ( 'keydown.ArrowUp' , [ '$event' ] ) @ HostListener ( 'keydown.ArrowLeft' , [ '$event' ] ) onDecreaseKey ( event ) { if ( this . isSplitterGutter ( event . target as HTMLElement ) ) { this . setGutterPosition ( event . target as HTMLElement , 1 ) ; event . preventDefault ( ) ; } } @ HostListener ( 'keydown.Home' , [ '$event' ] ) onHomeKey ( event ) { if ( this . isSplitterGutter ( event . target as HTMLElement ) ) { const areas = this . getAreasFromGutter ( event . target as HTMLElement ) ; const delta = areas . previous . size - areas . previous . minSize ; this . setGutterPosition ( event . target as HTMLElement , delta ) ; event . preventDefault ( ) ; } } @ HostListener ( 'keydown.End' , [ '$event' ] ) onEndKey ( event ) { if ( this . isSplitterGutter ( event . target as HTMLElement ) ) { const areas = this . getAreasFromGutter ( event . target as HTMLElement ) ; const delta = areas . next . size - areas . next . minSize ; this . setGutterPosition ( event . target as HTMLElement , - delta ) ; event . preventDefault ( ) ; } } /** Determine if an element is a gutter */ private isSplitterGutter ( element ) { return element . classList . contains ( 'as-split-gutter' ) || element . classList . contains ( 'as-split-gutter-icon' ) ; } /** Update the gutter position */ private setGutterPosition ( gutter , delta ) { const areas = this . getAreasFromGutter ( gutter ) ; if ( areas . previous . size - delta < areas . previous . minSize || areas . next . size + delta < areas . next . minSize ) { return ; } areas . previous . size -= delta ; areas . next . size += delta ; ( this . _splitter as any ) . refreshStyleSizes ( ) ; this . updateGutterAttributes ( ) ; } /** Get the split areas associated with a given gutter */ private getAreasFromGutter ( gutter ) { const index = this . _gutters . indexOf ( gutter ) ; return { previous : this . _splitter . displayedAreas [ index ] , next : this . _splitter . displayedAreas [ index + 1 ] } ; } }