import { MdcComponent , IValidatedElement , IError } from '@aurelia-mdc-web/base' ; import { cssClasses , MDCSelectFoundationMap , MDCSelectEventDetail , strings } from '@material/select' ; import { inject , useView , customElement , processContent , ViewCompiler , ViewResources , children , bindingMode , TaskQueue } from 'aurelia-framework' ; import { PLATFORM } from 'aurelia-pal' ; import { MdcSelectIcon , IMdcSelectIconElement , mdcIconStrings } from './mdc-select-icon' ; import { MdcSelectHelperText , mdcHelperTextCssClasses , IMdcSelectHelperTextElement } from './mdc-select-helper-text/mdc-select-helper-text' ; import { MdcLineRipple } from '@aurelia-mdc-web/line-ripple' ; import { MdcFloatingLabel } from '@aurelia-mdc-web/floating-label' ; import { MDCNotchedOutline } from '@material/notched-outline' ; import { MdcMenu } from '@aurelia-mdc-web/menu' ; import { MDCMenuItemEvent , Corner } from '@material/menu' ; import { bindable } from 'aurelia-typed-observable-plugin' ; import { MdcListItem , IMdcListItemElement } from '@aurelia-mdc-web/list' ; import { MDCSelectFoundationAurelia } from './mdc-select-foundation-aurelia' ; import { MDCSelectAdapterAurelia } from './mdc-select-adapter-aurelia' ; import { MDCMenuDistance } from '@material/menu-surface' ; import { MdcDefaultSelectConfiguration } from './mdc-default-select-configuration' ; strings . CHANGE_EVENT = strings . CHANGE_EVENT . toLowerCase ( ) ; let selectId = 0 ; selector emits @ inject ( Element , TaskQueue , MdcDefaultSelectConfiguration ) @ useView ( PLATFORM . moduleName ( './mdc-select.html' ) ) @ customElement ( cssClasses . ROOT ) @ processContent ( MdcSelect . processContent ) export class MdcSelect extends MdcComponent < MDCSelectFoundationAurelia > { static processContent ( _viewCompiler , _resources , element ) { const leadingIcon = element . querySelector ( `[${ mdcIconStrings . ATTRIBUTE }]` ) ; leadingIcon ?. setAttribute ( 'slot' , 'leading-icon' ) ; return true ; } constructor ( root , private taskQueue , private defaultConfiguration ) { super ( root ) ; defineMdcSelectElementApis ( this . root ) ; } id : string = `mdc-select-${ ++ selectId }` ; private menu : MdcMenu ; private selectAnchor : HTMLElement ; private selectedText : HTMLElement ; private menuElement ? : Element ; @ children ( 'mdc-list-items' ) items : MdcListItem [ ] ; private leadingIcon ? : MdcSelectIcon ; private helperText ? : MdcSelectHelperText ; private lineRipple ? : MdcLineRipple ; private mdcLabel : MdcFloatingLabel ; private outline ? : MDCNotchedOutline ; errors = new Map < IError , boolean > ( ) ; /** Sets the select label */ @ bindable label : string ; labelChanged ( ) { this . taskQueue . queueTask ( ( ) => this . foundation ?. layout ( ) ) ; } /** Styles the select as an outlined select */ @ bindable . booleanAttr outlined ? : boolean = this . defaultConfiguration . outlined ; outlinedChanged ( ) { this . taskQueue . queueTask ( ( ) => this . foundation ?. layout ( ) ) ; } /** Makes the value required */ @ bindable . booleanAttr required : boolean ; async requiredChanged ( ) { await this . initialised ; if ( this . required ) { this . selectAnchor ?. setAttribute ( 'aria-required' , 'true' ) ; } else { this . selectAnchor ?. removeAttribute ( 'aria-required' ) ; } this . foundation ?. setRequired ( this . required ) ; this . taskQueue . queueTask ( ( ) => this . foundation ?. layout ( ) ) ; } /** Enables/disables the select */ @ bindable . booleanAttr disabled : boolean ; async disabledChanged ( ) { await this . initialised ; this . foundation ?. setDisabled ( this . disabled ) ; } /** Hoists the select DOM to document.body */ @ bindable . booleanAttr ( { defaultBindingMode : bindingMode . oneTime } ) hoistToBody : boolean ; /** Sets the select DOM position to fixed */ @ bindable . booleanAttr ( { defaultBindingMode : bindingMode . oneTime } ) fixed : boolean ; /** Sets the margin between the select input and the dropdown */ @ bindable anchorMargin : Partial < MDCMenuDistance > ; /** Sets the select dropdown width to match content */ @ bindable . booleanAttr naturalWidth : boolean ; private initialValue : unknown ; get value ( ) : unknown { if ( this . foundation ) { return this . foundation . getValue ( ) ; } else { return this . initialValue ; } } set value ( value ) { this . setValue ( value ) ; } setValue ( value , skipNotify = false ) { if ( this . foundation ) { this . foundation . setValue ( value , skipNotify ) ; this . foundation . layout ( ) ; } else { this . initialValue = value ; } } get valid ( ) : boolean { return this . foundation ?. isValid ( ) ?? true ; } set valid ( value ) { this . foundation ?. setValid ( value ) ; } get selectedIndex ( ) : number { return this . foundation ! . getSelectedIndex ( ) ; } set selectedIndex ( selectedIndex ) { this . foundation ?. setSelectedIndex ( selectedIndex , true ) ; } addError ( error ) { this . errors . set ( error , true ) ; this . valid = false ; } removeError ( error ) { this . errors . delete ( error ) ; this . valid = this . errors . size === 0 ; } renderErrors ( ) { const helperText = this . root . nextElementSibling as IMdcSelectHelperTextElement ; if ( helperText ?. tagName === 'MDC-SELECT-HELPER-TEXT' ) { helperText . au . controller . viewModel . errors = Array . from ( this . errors . keys ( ) ) . filter ( x => x . message !== null ) . map ( x => x . message ! ) ; } } async initialise ( ) { const leadingIconEl = this . root . querySelector < IMdcSelectIconElement > ( `[${ mdcIconStrings . ATTRIBUTE }]` ) ; this . leadingIcon = leadingIconEl ?. au [ 'mdc-select-icon' ] . viewModel ; const nextSibling = this . root . nextElementSibling ; if ( nextSibling ?. tagName === mdcHelperTextCssClasses . ROOT . toUpperCase ( ) ) { this . helperText = ( nextSibling as IMdcSelectHelperTextElement ) . au . controller . viewModel ; } await Promise . all ( [ this . helperText ?. initialised , this . menu . initialised ] . filter ( x => x ) ) ; this . menu . list_ ! . singleSelection = true ; } initialSyncWithDOM ( ) { this . foundation ?. setValue ( this . initialValue , true ) ; this . foundation ?. layout ( ) ; this . errors = new Map < IError , boolean > ( ) ; this . valid = true ; } getDefaultFoundation ( ) { const adapter = { ... this . getSelectAdapterMethods ( ) , ... this . getCommonAdapterMethods ( ) , ... this . getOutlineAdapterMethods ( ) , ... this . getLabelAdapterMethods ( ) , } ; return new MDCSelectFoundationAurelia ( adapter , this . getFoundationMap ( ) ) ; } private getSelectAdapterMethods ( ) { return { 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 ; if ( ( this . hoistToBody || this . fixed ) && ! this . naturalWidth ) { this . menu . root . style . minWidth = this . menu . root . style . maxWidth = `${ this . root . clientWidth }px` ; } } , closeMenu : ( ) => { this . menu . open = false ; } , getAnchorElement : ( ) => this . root . querySelector ( strings . SELECT_ANCHOR_SELECTOR ) ! , setMenuAnchorElement : ( anchorEl ) => { this . menu . anchor = 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 ; } , removeAttributeAtIndex : ( index , attributeName ) => { this . menu . items [ index ] . removeAttribute ( attributeName ) ; } , focusMenuItemAtIndex : ( index ) => { ( this . menu . items [ index ] as HTMLElement ) . focus ( ) ; } , getMenuItemCount : ( ) => this . menu . items . length , getMenuItemValues : ( ) => this . menu . items . map ( x => ( x as IMdcListItemElement ) . au . controller . viewModel . value ) , 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 ?. setRippleCenter ( normalizedX ) , activateBottomLine : ( ) => this . lineRipple ?. activate ( ) , deactivateBottomLine : ( ) => this . lineRipple ?. deactivate ( ) , notifyChange : ( value ) => { const index = this . selectedIndex ; this . emit < MDCSelectEventDetail > ( strings . CHANGE_EVENT , { value , index } , true ) ; this . emit < MDCSelectEventDetail > ( 'change' , { value , index } , true ) ; } , } ; } private getOutlineAdapterMethods ( ) { return { hasOutline : ( ) => Boolean ( this . outline ) , notchOutline : ( labelWidth ) => this . outline ?. notch ( labelWidth ) , closeOutline : ( ) => this . outline ?. closeNotch ( ) , } ; } private getLabelAdapterMethods ( ) { return { hasLabel : ( ) => ! ! this . mdcLabel , floatLabel : ( shouldFloat ) => this . mdcLabel ?. float ( shouldFloat ) , getLabelWidth : ( ) => this . mdcLabel ? this . mdcLabel . getWidth ( ) : 0 , setLabelRequired : ( isRequired ) => this . mdcLabel ?. setRequired ( isRequired ) , } ; } handleChange ( ) { this . foundation ?. handleChange ( ) ; this . emit ( 'change' , { } , true ) ; } handleFocus ( ) { this . foundation ?. handleFocus ( ) ; } handleBlur ( ) { this . foundation ?. handleBlur ( ) ; if ( ! this . root . classList . contains ( cssClasses . FOCUSED ) ) { this . emit ( 'blur' , { } , true ) ; } } handleClick ( evt ) { this . selectAnchor . focus ( ) ; this . foundation ?. handleClick ( this . getNormalizedXCoordinate ( evt ) ) ; } handleKeydown ( evt ) { this . foundation ?. handleKeydown ( evt ) ; return true ; } handleMenuItemAction ( evt ) { this . foundation ?. handleMenuItemAction ( evt . detail . index ) ; } handleMenuOpened ( ) { this . foundation ?. handleMenuOpened ( ) ; } handleMenuClosed ( ) { this . foundation ?. handleMenuClosed ( ) ; if ( ! this . root . classList . contains ( cssClasses . FOCUSED ) ) { this . emit ( 'blur' , { } , true ) ; } } handleItemsChanged ( ) { this . foundation ?. layoutOptions ( ) ; this . foundation ?. layout ( ) ; } focus ( ) { this . selectAnchor . focus ( ) ; } blur ( ) { this . selectAnchor . blur ( ) ; } hidden 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 ) ; } hidden private getFoundationMap ( ) { return { helperText : this . helperText ?. foundation , leadingIcon : this . leadingIcon ?. foundation } ; } } hidden export interface IMdcSelectElement extends IValidatedElement { au : { controller : { viewModel : MdcSelect ; } ; } ; value : unknown ; } function defineMdcSelectElementApis ( element ) { Object . defineProperties ( element , { value : { get ( this ) { return this . au . controller . viewModel . value ; } , set ( this , value ) { this . au . controller . viewModel . value = value === '' ? undefined : value ; } , configurable : true } , options : { get ( this ) { return this . au . controller . viewModel . root . querySelectorAll ( '.mdc-list-item' ) ; } , configurable : true } , selectedIndex : { get ( this ) { return this . au . controller . viewModel . selectedIndex ; } , set ( this , value ) { this . au . controller . viewModel . selectedIndex = value ; } , configurable : true } , valid : { get ( this ) { return this . au . controller . viewModel . valid ; } , set ( this , value ) { this . au . controller . viewModel . valid = value ; } , configurable : true } , addError : { value ( this , error ) { this . au . controller . viewModel . addError ( error ) ; } , configurable : true } , removeError : { value ( this , error ) { this . au . controller . viewModel . removeError ( error ) ; } , configurable : true } , renderErrors : { value ( this ) { this . au . controller . viewModel . renderErrors ( ) ; } , configurable : true } , focus : { value ( this ) { this . au . controller . viewModel . focus ( ) ; } , configurable : true } , blur : { value ( this ) { this . au . controller . viewModel . blur ( ) ; } , configurable : true } } ) ; }