import { FocusableOption , FocusOrigin , Highlightable } from '@angular/cdk/a11y' ; import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ; import { ENTER , hasModifierKey , SPACE } from '@angular/cdk/keycodes' ; import { DOCUMENT } from '@angular/common' ; import { AfterViewChecked , ChangeDetectionStrategy , ChangeDetectorRef , Component , ElementRef , EventEmitter , HostListener , Inject , Input , OnDestroy , Optional , Output , QueryList , ViewEncapsulation , } from '@angular/core' ; import { Subject } from 'rxjs' ; import { TypeRef } from '../common-behaviors/type-ref' ; import { SbbOptgroup , SBB_OPTGROUP } from './optgroup' ; import { SbbOptionParentComponent , SBB_OPTION_PARENT_COMPONENT } from './option-parent' ; /**
 * Option IDs need to be unique across components, so this counter exists outside of
 * the component definition.
 */ let uniqueIdCounter = 0 ; /** Event object emitted by SbbOption when selected or deselected. */ export class SbbOptionSelectionChange { constructor ( /** Reference to the option that emitted the event. */ public source , /** Whether the change in the option's value was a result of a user action. */ public isUserInput = false ) { } } @ Component ( { selector : 'sbb-option' , exportAs : 'sbbOption' , styleUrls : [ 'option.css' ] , templateUrl : 'option.html' , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , host : { class : 'sbb-option sbb-menu-item sbb-link-reset' , role : 'option' , '[attr.tabindex]' : '_getTabIndex()' , '[class.sbb-selected]' : 'selected' , '[class.sbb-option-multiple]' : 'multiple' , '[class.sbb-focused]' : 'active' , '[id]' : 'id' , '[attr.aria-selected]' : '_getAriaSelected()' , '[attr.aria-disabled]' : 'disabled.toString()' , '[class.sbb-disabled]' : 'disabled' , } , } ) export class SbbOption implements AfterViewChecked , OnDestroy , FocusableOption , Highlightable { private _selected = false ; private _active = false ; private _disabled = false ; private _mostRecentViewValue = '' ; private _originalInnerHtml ? : string ; private _highlightValue ? : string ; private _highlighted = false ; /** Whether the wrapping component is in multiple selection mode. */ get multiple ( ) { return this . _parent && this . _parent . multiple ; } /** Whether or not the option is currently selected. */ get selected ( ) : boolean { return this . _selected ; } /** The form value of the option. */ @ Input ( ) value : any ; /** The unique ID of the option. */ @ Input ( ) id : string = `sbb-option-${ uniqueIdCounter ++ }` ; /** Whether the option is disabled. */ @ Input ( ) get disabled ( ) : boolean { return ( this . group && this . group . disabled ) || this . _disabled ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; this . _changeDetectorRef . markForCheck ( ) ; } /** Event emitted when the option is selected or deselected. */ @ Output ( ) readonly onSelectionChange = new EventEmitter < SbbOptionSelectionChange > ( ) ; /** Emits when the state of the option changes and any parents have to be notified. */ readonly _stateChanges = new Subject < void > ( ) ; constructor ( private _element , @ Inject ( DOCUMENT ) private _document , private _changeDetectorRef , @ Optional ( ) @ Inject ( SBB_OPTION_PARENT_COMPONENT ) private _parent , @ Optional ( ) @ Inject ( SBB_OPTGROUP ) readonly group ) { } /**
     * Whether or not the option is currently active and ready to be selected.
     * An active option displays styles as if it is focused, but the
     * focus is actually retained somewhere else. This comes in handy
     * for components like autocomplete where focus must remain on the input.
     */ get active ( ) : boolean { return this . _active ; } /**
     * The displayed value of the option. It is necessary to show the selected option in the
     * select's trigger.
     */ get viewValue ( ) : string { return ( this . _getHostElement ( ) . textContent || '' ) . trim ( ) ; } /** Selects the option. */ select ( ) { if ( ! this . selected ) { this . _selected = true ; this . _changeDetectorRef . markForCheck ( ) ; this . _emitSelectionChangeEvent ( ) ; } } /** Deselects the option. */ deselect ( ) { if ( this . _selected ) { this . _selected = false ; this . _changeDetectorRef . markForCheck ( ) ; this . _emitSelectionChangeEvent ( ) ; } } /** Sets focus onto this option. */ focus ( _origin ? , options ? ) { const element = this . _getHostElement ( ) ; if ( typeof element . focus === 'function' ) { element . focus ( options ) ; } } /**
     * This method sets display styles on the option to make it appear
     * active. This is used by the ActiveDescendantKeyManager so key
     * events will display the proper options as active on arrow key events.
     */ setActiveStyles ( ) { if ( ! this . active ) { this . _active = true ; this . _changeDetectorRef . markForCheck ( ) ; } } /**
     * This method removes display styles on the option that made it appear
     * active. This is used by the ActiveDescendantKeyManager so key
     * events will display the proper options as active on arrow key events.
     */ setInactiveStyles ( ) { if ( this . active ) { this . _active = false ; this . _changeDetectorRef . markForCheck ( ) ; } } /** Gets the label to be used when determining whether the option should be focused. */ getLabel ( ) { return this . viewValue ; } /** Ensures the option is selected when activated from the keyboard. */ @ HostListener ( 'keydown' , [ '$event' ] ) _handleKeydown ( event ) { if ( ( event . keyCode === ENTER || event . keyCode === SPACE ) && ! hasModifierKey ( event ) ) { this . _selectViaInteraction ( ) ; event . preventDefault ( ) ; } } /**
     * `Selects the option while indicating the selection came from the user. Used to
     * determine if the select's view -> model callback should be invoked.`
     */ @ HostListener ( 'click' ) _selectViaInteraction ( ) { if ( ! this . disabled ) { this . _selected = this . multiple ? ! this . selected : true ; this . _changeDetectorRef . markForCheck ( ) ; this . _emitSelectionChangeEvent ( true ) ; } } /**
     * Gets the `aria-selected` value for the option. We explicitly omit the `aria-selected`
     * attribute from single-selection, unselected options. Including the `aria-selected="false"`
     * attributes adds a significant amount of noise to screen-reader users without providing useful
     * information.
     */ _getAriaSelected ( ) { return this . selected || ( this . multiple ? false : null ) ; } /** Returns the correct tabindex for the option depending on disabled state. */ _getTabIndex ( ) { return this . disabled ? '-1' : '0' ; } /** Gets the host DOM element. */ _getHostElement ( ) { return this . _element . nativeElement ; } ngAfterViewChecked ( ) { if ( this . _selected ) { const viewValue = this . viewValue ; if ( viewValue !== this . _mostRecentViewValue ) { this . _mostRecentViewValue = viewValue ; this . _stateChanges . next ( ) ; } } } ngOnDestroy ( ) { this . _stateChanges . complete ( ) ; } docs-private _highlight ( value , localeNormalizer ? ) { if ( this . _originalInnerHtml === undefined ) { this . _originalInnerHtml = this . _element . nativeElement . innerHTML ; } else if ( value === this . _highlightValue ) { return ; } else if ( this . _highlighted ) { this . _element . nativeElement . innerHTML = this . _originalInnerHtml ; this . _highlighted = false ; } this . _highlightValue = value ; if ( value && this . _originalInnerHtml ) { const normalizer = localeNormalizer || ( ( v ) => v ) ; value = normalizer ( value ) ; const escapedValue = value . replace ( /[-[\]{}()*+?.,\\^$|#\s]/g , '\\$&' ) ; const matcher = new RegExp ( `${ escapedValue }+` , 'i' ) ; const nodes = this . _findAllTextNodesWithMatch ( matcher , normalizer ) ; nodes . forEach ( ( n ) => this . _highlightNode ( n , matcher , normalizer ) ) ; this . _highlighted = ! ! nodes . length ; } } private _findAllTextNodesWithMatch ( matcher , localeNormalizer , node = this . _element . nativeElement ) { const nodes = [ ] ; const childNodes = node . childNodes ; for ( let i = 0 ; i < childNodes . length ; i ++ ) { const childNode = childNodes [ i ] ; if ( childNode . nodeType === 3 && matcher . test ( localeNormalizer ( childNode . textContent || '' ) ) ) { nodes . push ( childNode ) ; } else if ( childNode . childNodes . length ) { nodes . push ( ... this . _findAllTextNodesWithMatch ( matcher , localeNormalizer , childNode ) ) ; } } return nodes ; } param node param matcher param localeNormalizer private _highlightNode ( node , matcher , localeNormalizer ) { const nodes = [ ] ; const doc = this . _document ; matcher . lastIndex = 0 ; let text = node . textContent || '' ; let match ; do { match = localeNormalizer ( text ) . match ( matcher ) ; if ( ! match ) { nodes . push ( doc . createTextNode ( text ) ) ; continue ; } else if ( match . index ) { nodes . push ( doc . createTextNode ( text . substring ( 0 , match . index ) ) ) ; text = text . substring ( match . index ) ; } const strong = doc . createElement ( 'strong' ) ; strong . textContent = text . substring ( 0 , match [ 0 ] . length ) ; nodes . push ( strong ) ; text = text . substring ( match [ 0 ] . length ) ; } while ( match ) ; const parent = node . parentNode ! ; nodes . forEach ( ( n ) => parent . insertBefore ( n , node ) ) ; parent . removeChild ( node ) ; } /** Emits the selection change event. */ private _emitSelectionChangeEvent ( isUserInput = false ) { this . onSelectionChange . emit ( new SbbOptionSelectionChange ( this , isUserInput ) ) ; } } param optionIndex param options param optionGroups docs-private export function countGroupLabelsBeforeOption ( optionIndex , options , optionGroups ) { if ( optionGroups . length ) { const optionsArray = options . toArray ( ) ; const groups = optionGroups . toArray ( ) ; let groupCounter = 0 ; for ( let i = 0 ; i < optionIndex + 1 ; i ++ ) { if ( optionsArray [ i ] . group && optionsArray [ i ] . group === groups [ groupCounter ] ) { groupCounter ++ ; } } return groupCounter ; } return 0 ; } param optionOffset param optionHeight param currentScrollPosition param panelHeight docs-private export function getOptionScrollPosition ( optionOffset , optionHeight , currentScrollPosition , panelHeight ) { if ( optionOffset < currentScrollPosition ) { return optionOffset ; } if ( optionOffset + optionHeight > currentScrollPosition + panelHeight ) { return Math . max ( 0 , optionOffset - panelHeight + optionHeight ) ; } return currentScrollPosition ; }