license import { MDCComponent } from '@material/base/component' ; import { CustomEventListener , SpecificEventListener } from '@material/base/types' ; import { MDCFloatingLabel , MDCFloatingLabelFactory } from '@material/floating-label/component' ; import { MDCLineRipple , MDCLineRippleFactory } from '@material/line-ripple/component' ; import * as menuSurfaceConstants from '@material/menu-surface/constants' ; import { MDCMenu , MDCMenuFactory } from '@material/menu/component' ; import * as menuConstants from '@material/menu/constants' ; import { MDCMenuItemEvent } from '@material/menu/types' ; import { MDCNotchedOutline , MDCNotchedOutlineFactory } from '@material/notched-outline/component' ; import { MDCRippleAdapter } from '@material/ripple/adapter' ; import { MDCRipple } from '@material/ripple/component' ; import { MDCRippleFoundation } from '@material/ripple/foundation' ; import { MDCSelectAdapter } from './adapter' ; import { cssClasses , strings } from './constants' ; import { MDCSelectFoundation } from './foundation' ; import { MDCSelectHelperText , MDCSelectHelperTextFactory } from './helper-text/component' ; import { MDCSelectIcon , MDCSelectIconFactory } from './icon/component' ; import { MDCSelectEventDetail , MDCSelectFoundationMap } from './types' ; export class MDCSelect extends MDCComponent < MDCSelectFoundation > { static override attachTo ( root ) { return new MDCSelect ( root ) ; } private ripple ! : MDCRipple | null ; private menu ! : MDCMenu ; private selectAnchor ! : HTMLElement ; private selectedText ! : HTMLElement ; private hiddenInput ! : HTMLInputElement | null ; private menuElement ! : Element ; private menuItemValues ! : string [ ] ; private leadingIcon ? : MDCSelectIcon ; private helperText ! : MDCSelectHelperText | null ; private lineRipple ! : MDCLineRipple | null ; private label ! : MDCFloatingLabel | null ; private outline ! : MDCNotchedOutline | null ; private handleFocus ! : SpecificEventListener < 'focus' > ; private handleBlur ! : SpecificEventListener < 'blur' > ; private handleClick ! : SpecificEventListener < 'click' > ; private handleKeydown ! : SpecificEventListener < 'keydown' > ; private handleMenuOpened ! : EventListener ; private handleMenuClosed ! : EventListener ; private handleMenuClosing ! : EventListener ; private handleMenuItemAction ! : CustomEventListener < MDCMenuItemEvent > ; override initialize ( labelFactory = ( el ) => new MDCFloatingLabel ( el ) , lineRippleFactory = ( el ) => new MDCLineRipple ( el ) , outlineFactory = ( el ) => new MDCNotchedOutline ( el ) , menuFactory = ( el ) => new MDCMenu ( el ) , iconFactory = ( el ) => new MDCSelectIcon ( el ) , helperTextFactory = ( el ) => new MDCSelectHelperText ( el ) ) { this . selectAnchor = this . root . querySelector ( strings . SELECT_ANCHOR_SELECTOR ) as HTMLElement ; this . selectedText = this . root . querySelector ( strings . SELECTED_TEXT_SELECTOR ) as HTMLElement ; this . hiddenInput = this . root . querySelector ( strings . HIDDEN_INPUT_SELECTOR ) as HTMLInputElement ; if ( ! this . selectedText ) { throw new Error ( 'MDCSelect: Missing required element: The following selector must be present: ' + `'${ strings . SELECTED_TEXT_SELECTOR }'` ) ; } if ( this . selectAnchor . hasAttribute ( strings . ARIA_CONTROLS ) ) { const helperTextElement = document . getElementById ( this . selectAnchor . getAttribute ( strings . ARIA_CONTROLS ) ! ) ; if ( helperTextElement ) { this . helperText = helperTextFactory ( helperTextElement ) ; } } this . menuSetup ( menuFactory ) ; const labelElement = this . root . querySelector ( strings . LABEL_SELECTOR ) ; this . label = labelElement ? labelFactory ( labelElement ) : null ; const lineRippleElement = this . root . querySelector ( strings . LINE_RIPPLE_SELECTOR ) ; this . lineRipple = lineRippleElement ? lineRippleFactory ( lineRippleElement ) : null ; const outlineElement = this . root . querySelector ( strings . OUTLINE_SELECTOR ) ; this . outline = outlineElement ? outlineFactory ( outlineElement ) : null ; const leadingIcon = this . root . querySelector ( strings . LEADING_ICON_SELECTOR ) ; if ( leadingIcon ) { this . leadingIcon = iconFactory ( leadingIcon ) ; } if ( ! this . root . classList . contains ( cssClasses . OUTLINED ) ) { this . ripple = this . createRipple ( ) ; } } /**
     * Initializes the select's event listeners and internal state based
     * on the environment's state.
     */ override initialSyncWithDOM ( ) { this . handleFocus = ( ) => { this . foundation . handleFocus ( ) ; } ; this . handleBlur = ( ) => { this . foundation . handleBlur ( ) ; } ; this . handleClick = ( evt ) => { this . selectAnchor . focus ( ) ; this . foundation . handleClick ( this . getNormalizedXCoordinate ( evt ) ) ; } ; this . handleKeydown = ( evt ) => { this . foundation . handleKeydown ( evt ) ; } ; this . handleMenuItemAction = ( evt ) => { this . foundation . handleMenuItemAction ( evt . detail . index ) ; } ; this . handleMenuOpened = ( ) => { this . foundation . handleMenuOpened ( ) ; } ; this . handleMenuClosed = ( ) => { this . foundation . handleMenuClosed ( ) ; } ; this . handleMenuClosing = ( ) => { this . foundation . handleMenuClosing ( ) ; } ; this . selectAnchor . addEventListener ( 'focus' , this . handleFocus ) ; this . selectAnchor . addEventListener ( 'blur' , this . handleBlur ) ; this . selectAnchor . addEventListener ( 'click' , this . handleClick as EventListener ) ; this . selectAnchor . addEventListener ( 'keydown' , this . handleKeydown ) ; this . menu . listen ( menuSurfaceConstants . strings . CLOSED_EVENT , this . handleMenuClosed ) ; this . menu . listen ( menuSurfaceConstants . strings . CLOSING_EVENT , this . handleMenuClosing ) ; this . menu . listen ( menuSurfaceConstants . strings . OPENED_EVENT , this . handleMenuOpened ) ; this . menu . listen ( menuConstants . strings . SELECTED_EVENT , this . handleMenuItemAction ) ; if ( this . hiddenInput ) { if ( this . hiddenInput . value ) { this . foundation . setValue ( this . hiddenInput . value , true ) ; this . foundation . layout ( ) ; return ; } this . hiddenInput . value = this . value ; } } override destroy ( ) { this . selectAnchor . removeEventListener ( 'focus' , this . handleFocus ) ; this . selectAnchor . removeEventListener ( 'blur' , this . handleBlur ) ; this . selectAnchor . removeEventListener ( 'keydown' , this . handleKeydown ) ; this . selectAnchor . removeEventListener ( 'click' , this . handleClick as EventListener ) ; this . menu . unlisten ( menuSurfaceConstants . strings . CLOSED_EVENT , this . handleMenuClosed ) ; this . menu . unlisten ( menuSurfaceConstants . strings . OPENED_EVENT , this . handleMenuOpened ) ; this . menu . unlisten ( menuConstants . strings . SELECTED_EVENT , this . handleMenuItemAction ) ; this . menu . destroy ( ) ; if ( this . ripple ) { this . ripple . destroy ( ) ; } if ( this . outline ) { this . outline . destroy ( ) ; } if ( this . leadingIcon ) { this . leadingIcon . destroy ( ) ; } if ( this . helperText ) { this . helperText . destroy ( ) ; } super . destroy ( ) ; } get value ( ) : string { return this . foundation . getValue ( ) ; } set value ( value ) { this . foundation . setValue ( value ) ; } setValue ( value , skipNotify = false ) { this . foundation . setValue ( value , skipNotify ) ; } get selectedIndex ( ) : number { return this . foundation . getSelectedIndex ( ) ; } set selectedIndex ( selectedIndex ) { this . foundation . setSelectedIndex ( selectedIndex , true ) ; } setSelectedIndex ( selectedIndex , skipNotify = false ) { this . foundation . setSelectedIndex ( selectedIndex , true , skipNotify ) ; } get disabled ( ) : boolean { return this . foundation . getDisabled ( ) ; } set disabled ( disabled ) { this . foundation . setDisabled ( disabled ) ; if ( this . hiddenInput ) { this . hiddenInput . disabled = disabled ; } } set leadingIconAriaLabel ( label ) { this . foundation . setLeadingIconAriaLabel ( label ) ; } /**
     * Sets the text content of the leading icon.
     */ set leadingIconContent ( content ) { this . foundation . setLeadingIconContent ( content ) ; } /**
     * Sets the text content of the helper text.
     */ set helperTextContent ( content ) { this . foundation . setHelperTextContent ( content ) ; } param useDefaultValidation set useDefaultValidation ( useDefaultValidation ) { this . foundation . setUseDefaultValidation ( useDefaultValidation ) ; } /**
     * Sets the current invalid state of the select.
     */ set valid ( isValid ) { this . foundation . setValid ( isValid ) ; } /**
     * Checks if the select is in a valid state.
     */ get valid ( ) : boolean { return this . foundation . isValid ( ) ; } /**
     * Sets the control to the required state.
     */ set required ( isRequired ) { this . foundation . setRequired ( isRequired ) ; } /**
     * Returns whether the select is required.
     */ get required ( ) : boolean { return this . foundation . getRequired ( ) ; } /**
     * Re-calculates if the notched outline should be notched and if the label
     * should float.
     */ layout ( ) { this . foundation . layout ( ) ; } /**
     * Synchronizes the list of options with the state of the foundation. Call
     * this whenever menu options are dynamically updated.
     */ layoutOptions ( ) { this . foundation . layoutOptions ( ) ; this . menu . layout ( ) ; this . menuItemValues = this . menu . items . map ( ( el ) => el . getAttribute ( strings . VALUE_ATTR ) || '' ) ; if ( this . hiddenInput ) { this . hiddenInput . value = this . value ; } } override getDefaultFoundation ( ) { const adapter = { ... this . getSelectAdapterMethods ( ) , ... this . getCommonAdapterMethods ( ) , ... this . getOutlineAdapterMethods ( ) , ... this . getLabelAdapterMethods ( ) , } ; return new MDCSelectFoundation ( adapter , this . getFoundationMap ( ) ) ; } /**
     * Handles setup for the menu.
     */ private menuSetup ( menuFactory ) { this . menuElement = this . root . querySelector ( strings . MENU_SELECTOR ) ! ; this . menu = menuFactory ( this . menuElement ) ; this . menu . hasTypeahead = true ; this . menu . singleSelection = true ; this . menuItemValues = this . menu . items . map ( ( el ) => el . getAttribute ( strings . VALUE_ATTR ) || '' ) ; } private createRipple ( ) { const adapter = { ... MDCRipple . createAdapter ( { root : this . selectAnchor } ) , registerInteractionHandler : ( evtType , handler ) => { this . selectAnchor . addEventListener ( evtType , handler ) ; } , deregisterInteractionHandler : ( evtType , handler ) => { this . selectAnchor . removeEventListener ( evtType , handler ) ; } , } ; return new MDCRipple ( this . selectAnchor , new MDCRippleFoundation ( adapter ) ) ; } private getSelectAdapterMethods ( ) { return { getMenuItemAttr : ( menuItem , attr ) => menuItem . getAttribute ( attr ) , setSelectedText : ( text ) => { this . selectedText . textContent = text ; } , isSelectAnchorFocused : ( ) => document . activeElement === this . selectAnchor , getSelectAnchorAttr : ( attr ) => this . selectAnchor . getAttribute ( attr ) , setSelectAnchorAttr : ( attr , value ) => { this . selectAnchor . setAttribute ( attr , value ) ; } , removeSelectAnchorAttr : ( attr ) => { this . selectAnchor . removeAttribute ( attr ) ; } , addMenuClass : ( className ) => { this . menuElement . classList . add ( className ) ; } , removeMenuClass : ( className ) => { this . menuElement . classList . remove ( className ) ; } , openMenu : ( ) => { this . menu . open = true ; } , closeMenu : ( ) => { this . menu . open = false ; } , getAnchorElement : ( ) => this . root . querySelector ( strings . SELECT_ANCHOR_SELECTOR ) ! , setMenuAnchorElement : ( anchorEl ) => { this . menu . setAnchorElement ( anchorEl ) ; } , setMenuAnchorCorner : ( anchorCorner ) => { this . menu . setAnchorCorner ( anchorCorner ) ; } , setMenuWrapFocus : ( wrapFocus ) => { this . menu . wrapFocus = wrapFocus ; } , getSelectedIndex : ( ) => { const index = this . menu . selectedIndex ; return index instanceof Array ? index [ 0 ] : index ; } , setSelectedIndex : ( index ) => { this . menu . selectedIndex = index ; } , focusMenuItemAtIndex : ( index ) => { ( this . menu . items [ index ] as HTMLElement ) . focus ( ) ; } , getMenuItemCount : ( ) => this . menu . items . length , getMenuItemValues : ( ) => this . menuItemValues , getMenuItemTextAtIndex : ( index ) => this . menu . getPrimaryTextAtIndex ( index ) , isTypeaheadInProgress : ( ) => this . menu . typeaheadInProgress , typeaheadMatchItem : ( nextChar , startingIndex ) => this . menu . typeaheadMatchItem ( nextChar , startingIndex ) , } ; } private getCommonAdapterMethods ( ) { return { addClass : ( className ) => { this . root . classList . add ( className ) ; } , removeClass : ( className ) => { this . root . classList . remove ( className ) ; } , hasClass : ( className ) => this . root . classList . contains ( className ) , setRippleCenter : ( normalizedX ) => { this . lineRipple && this . lineRipple . setRippleCenter ( normalizedX ) ; } , activateBottomLine : ( ) => { this . lineRipple && this . lineRipple . activate ( ) ; } , deactivateBottomLine : ( ) => { this . lineRipple && this . lineRipple . deactivate ( ) ; } , notifyChange : ( value ) => { if ( this . hiddenInput ) { this . hiddenInput . value = value ; } const index = this . selectedIndex ; this . emit < MDCSelectEventDetail > ( strings . CHANGE_EVENT , { value , index } , true ) ; } , } ; } private getOutlineAdapterMethods ( ) { return { hasOutline : ( ) => Boolean ( this . outline ) , notchOutline : ( labelWidth ) => { this . outline && this . outline . notch ( labelWidth ) ; } , closeOutline : ( ) => { this . outline && this . outline . closeNotch ( ) ; } , } ; } private getLabelAdapterMethods ( ) { return { hasLabel : ( ) => ! ! this . label , floatLabel : ( shouldFloat ) => { this . label && this . label . float ( shouldFloat ) ; } , getLabelWidth : ( ) => this . label ? this . label . getWidth ( ) : 0 , setLabelRequired : ( isRequired ) => { this . label && this . label . setRequired ( isRequired ) ; } , } ; } /**
     * Calculates where the line ripple should start based on the x coordinate within the component.
     */ private getNormalizedXCoordinate ( evt ) { const targetClientRect = ( evt . target as Element ) . getBoundingClientRect ( ) ; const xCoordinate = this . isTouchEvent ( evt ) ? evt . touches [ 0 ] . clientX : evt . clientX ; return xCoordinate - targetClientRect . left ; } private isTouchEvent ( evt ) { return Boolean ( ( evt as TouchEvent ) . touches ) ; } /**
     * Returns a map of all subcomponents to subfoundations.
     */ private getFoundationMap ( ) { return { helperText : this . helperText ? this . helperText . foundationForSelect : undefined , leadingIcon : this . leadingIcon ? this . leadingIcon . foundationForSelect : undefined , } ; } }