import { MdcComponent } from '@aurelia-mdc-web/base' ; import { MDCListFoundation , MDCListAdapter , strings , cssClasses , MDCListIndex } from '@material/list' ; import { inject , useView , customElement , children } from 'aurelia-framework' ; import { PLATFORM } from 'aurelia-pal' ; import { closest , matches } from '@material/dom/ponyfill' ; import { bindable } from 'aurelia-typed-observable-plugin' ; import { MdcListItem , IMdcListItemElement , IMdcListActionEventDetail } from './mdc-list-item/mdc-list-item' ; strings . ACTION_EVENT = strings . ACTION_EVENT . toLowerCase ( ) ; export const mdcListStrings = { ITEMS_CHANGED : 'mdclist:itemschanged' } ; selector emits emits @ inject ( Element ) @ useView ( PLATFORM . moduleName ( './mdc-list.html' ) ) @ customElement ( cssClasses . ROOT ) export class MdcList extends MdcComponent < MDCListFoundation > { cssClasses = cssClasses ; /** When enabled, the space and enter keys (or click event) will trigger an single list item to become selected and any other previous selected element to become deselected */ @ bindable . booleanAttr singleSelection : boolean ; async singleSelectionChanged ( ) { await this . initialised ; this . foundation ?. setSingleSelection ( this . singleSelection ) ; } /** Sets the selection logic to apply/remove the mdc-list-item--activated class */ @ bindable . booleanAttr activated : boolean ; async activatedChanged ( ) { await this . initialised ; this . foundation ?. setUseActivatedClass ( this . activated ) ; } @ children ( 'mdc-list-item' ) items : MdcListItem [ ] ; itemsChanged ( ) { this . foundation ?. layout ( ) ; this . emit ( mdcListStrings . ITEMS_CHANGED , { items : this . items } , true ) ; } @ bindable . booleanAttr typeahead : boolean ; async typeaheadChanged ( hasTypeahead ) { await this . initialised ; this . foundation ?. setHasTypeahead ( hasTypeahead ) ; } /** Sets the list to allow the up arrow on the first element to focus the last element of the list and vice versa */ @ bindable . booleanAttr wrapFocus : boolean ; async wrapFocusChanged ( ) { await this . initialised ; this . foundation ?. setWrapFocus ( this . wrapFocus ) ; } initialSyncWithDOM ( ) { this . layout ( ) ; this . initializeListType ( ) ; } get listElements ( ) : Element [ ] { return Array . from ( this . root . querySelectorAll ( `.${ cssClasses . LIST_ITEM_CLASS }` ) ) ; } param item return getPrimaryText ( item ) { const primaryText = item . querySelector ( `.${ cssClasses . LIST_ITEM_PRIMARY_TEXT_CLASS }` ) ; if ( primaryText ) { return primaryText . textContent ?? '' ; } const singleLineText = item . querySelector ( `.${ cssClasses . LIST_ITEM_TEXT_CLASS }` ) ; return singleLineText ?. textContent ?? '' ; } getDefaultFoundation ( ) { const adapter = { addClassForElementIndex : ( index , className ) => { const element = this . listElements [ index ] ; if ( element ) { element . classList . add ( className ) ; } } , focusItemAtIndex : ( index ) => { const element = this . listElements [ index ] as HTMLElement | undefined ; if ( element ) { element . focus ( ) ; } } , getAttributeForElementIndex : ( index , attr ) => this . listElements [ index ] . getAttribute ( attr ) , getFocusedElementIndex : ( ) => this . listElements . indexOf ( document . activeElement ! ) , getListItemCount : ( ) => this . listElements . length , getPrimaryTextAtIndex : ( index ) => this . getPrimaryText ( this . listElements [ index ] ) , hasCheckboxAtIndex : ( index ) => { const listItem = this . listElements [ index ] ; return ! ! listItem . querySelector ( strings . CHECKBOX_SELECTOR ) ; } , hasRadioAtIndex : ( index ) => { const listItem = this . listElements [ index ] ; return ! ! listItem . querySelector ( strings . RADIO_SELECTOR ) ; } , isCheckboxCheckedAtIndex : ( index ) => { const listItem = this . listElements [ index ] ; const toggleEl = listItem . querySelector < HTMLInputElement > ( strings . CHECKBOX_SELECTOR ) ; return toggleEl ! . checked ; } , isFocusInsideList : ( ) => { return this . root !== document . activeElement && this . root . contains ( document . activeElement ) ; } , isRootFocused : ( ) => document . activeElement === this . root , listItemAtIndexHasClass : ( index , className ) => this . listElements [ index ] . classList . contains ( className ) , notifyAction : ( index ) => { const listItem = this . listElements [ index ] ; if ( ! listItem . hasAttribute ( 'no-list-action' ) ) { const data = ( listItem as IMdcListItemElement ) . au . controller . viewModel . value ; this . emit < IMdcListActionEventDetail > ( strings . ACTION_EVENT , { index , data } , true ) ; } } , removeClassForElementIndex : ( index , className ) => { const element = this . listElements [ index ] ; if ( element ) { element . classList . remove ( className ) ; } } , setAttributeForElementIndex : ( index , attr , value ) => { const element = this . listElements [ index ] ; if ( element ) { element . setAttribute ( attr , value ) ; } } , setCheckedCheckboxOrRadioAtIndex : ( index , isChecked ) => { const listItem = this . listElements [ index ] ; const toggleEl = listItem . querySelector < HTMLInputElement > ( strings . CHECKBOX_RADIO_SELECTOR ) ; if ( toggleEl ?. disabled ) { return ; } toggleEl ! . checked = isChecked ; const event = document . createEvent ( 'Event' ) ; event . initEvent ( 'change' , true , true ) ; toggleEl ! . dispatchEvent ( event ) ; } , setTabIndexForListItemChildren : ( listItemIndex , tabIndexValue ) => { const element = this . listElements [ listItemIndex ] ; const listItemChildren = [ ] . slice . call ( element . querySelectorAll ( strings . CHILD_ELEMENTS_TO_TOGGLE_TABINDEX ) ) ; listItemChildren . forEach ( ( el ) => el . setAttribute ( 'tabindex' , tabIndexValue ) ) ; } , } ; return new MDCListFoundation ( adapter ) ; } hidden private getListItemIndex_ ( evt ) { const eventTarget = evt . target as Element ; const nearestParent = closest ( eventTarget , `.${ cssClasses . LIST_ITEM_CLASS }, .${ cssClasses . ROOT }` ) ; if ( nearestParent && matches ( nearestParent , `.${ cssClasses . LIST_ITEM_CLASS }` ) ) { return this . listElements . indexOf ( nearestParent ) ; } return - 1 ; } hidden handleFocusInEvent_ ( evt ) { const index = this . getListItemIndex_ ( evt ) ; this . foundation ?. handleFocusIn ( index ) ; } hidden handleFocusOutEvent_ ( evt ) { const index = this . getListItemIndex_ ( evt ) ; this . foundation ?. handleFocusOut ( index ) ; } hidden handleKeydownEvent_ ( evt ) { const index = this . getListItemIndex_ ( evt ) ; const target = evt . target as Element ; if ( ! target . hasAttribute ( 'not-selectable' ) ) { this . foundation ?. handleKeydown ( evt , target . classList . contains ( cssClasses . LIST_ITEM_CLASS ) , index ) ; } return true ; } hidden handleClickEvent_ ( evt ) { const index = this . getListItemIndex_ ( evt ) ; const target = evt . target as Element ; const toggleCheckbox = ! matches ( target , strings . CHECKBOX_RADIO_SELECTOR ) ; this . foundation ?. handleClick ( index , toggleCheckbox ) ; return true ; } hidden return get typeaheadInProgress ( ) : boolean { return this . foundation ! . isTypeaheadInProgress ( ) ; } hidden param nextChar param startingIndex return typeaheadMatchItem ( nextChar , startingIndex ? ) { return this . foundation ! . typeaheadMatchItem ( nextChar , startingIndex , true ) ; } layout ( ) { this . foundation ?. layout ( ) ; } get selectedIndex ( ) : MDCListIndex { return this . foundation ! . getSelectedIndex ( ) ; } set selectedIndex ( index ) { this . foundation ?. setSelectedIndex ( index ) ; } hidden initializeListType ( ) { const checkboxListItems = this . root . querySelectorAll ( strings . ARIA_ROLE_CHECKBOX_SELECTOR ) ; const radioSelectedListItem = this . root . querySelector ( strings . ARIA_CHECKED_RADIO_SELECTOR ) ; if ( checkboxListItems . length ) { const preselectedItems = this . root . querySelectorAll ( strings . ARIA_CHECKED_CHECKBOX_SELECTOR ) ; this . selectedIndex = [ ] . map . call ( preselectedItems , ( listItem ) => this . listElements . indexOf ( listItem ) ) as number [ ] ; } else if ( radioSelectedListItem ) { this . selectedIndex = this . listElements . indexOf ( radioSelectedListItem ) ; } } } hidden export interface IMdcListElement extends HTMLElement { au : { controller : { viewModel : MdcList ; } ; } ; }