import { MdcComponent } from '@aurelia-mdc-web/base' ; import { MDCDataTableFoundation , selectors , MDCDataTableAdapter , events , cssClasses , dataAttributes , MDCDataTableRowSelectionChangedEventDetail , SortValue , messages , RowClickEventData } from '@material/data-table' ; import { MdcCheckbox , IMdcCheckboxElement } from '@aurelia-mdc-web/checkbox' ; import { closest } from '@material/dom/ponyfill' ; import { inject , customElement , useView , PLATFORM , ViewCompiler , ViewResources , bindingMode , computedFrom , processContent } from 'aurelia-framework' ; import { bindable } from 'aurelia-typed-observable-plugin' ; events . ROW_CLICK = events . ROW_CLICK . toLowerCase ( ) ; events . ROW_SELECTION_CHANGED = events . ROW_SELECTION_CHANGED . toLowerCase ( ) ; events . SELECTED_ALL = events . SELECTED_ALL . toLowerCase ( ) ; events . SORTED = events . SORTED . toLowerCase ( ) ; events . UNSELECTED_ALL = events . UNSELECTED_ALL . toLowerCase ( ) ; const NAVIGATION_EVENT = 'mdcdatatable:navigation' ; selector emits emits emits emits emits @ inject ( Element ) @ useView ( PLATFORM . moduleName ( './mdc-data-table.html' ) ) @ customElement ( 'mdc-data-table' ) @ processContent ( MdcDataTable . processContent ) export class MdcDataTable extends MdcComponent < MDCDataTableFoundation > implements EventListenerObject { static processContent ( _viewCompiler , _resources , element ) { const table = element . querySelector ( 'table' ) ; if ( ! table ) { throw new Error ( 'Have you forgotten the <table> tag in you data table markup?' ) ; } table . classList . add ( 'mdc-data-table__table' ) ; table . setAttribute ( 'aria-label' , '${ariaLabel}' ) ; const headerRow = element . querySelector ( 'thead>tr' ) ; if ( ! headerRow ) { throw new Error ( 'Have you forgotten the <thead><tr> tags in you data table markup?' ) ; } headerRow . classList . add ( cssClasses . HEADER_ROW ) ; headerRow . setAttribute ( 'ref' , 'headerRow' ) ; const headerCells = headerRow . querySelectorAll < HTMLElement > ( 'th' ) ; for ( const th of Array . from ( headerCells ) ) { th . classList . add ( cssClasses . HEADER_CELL ) ; th . classList . toggle ( 'mdc-data-table__header-cell--numeric' , th . hasAttribute ( 'numeric' ) ) ; th . setAttribute ( 'role' , 'columnheader' ) ; th . setAttribute ( 'scope' , 'col' ) ; } const tbody = element . querySelector ( 'tbody' ) ; if ( ! tbody ) { throw new Error ( 'Have you forgotten the <tbody> tag in you data table markup?' ) ; } tbody . classList . add ( cssClasses . CONTENT ) ; tbody . setAttribute ( 'ref' , 'content' ) ; return true ; } header : HTMLElement ; content : HTMLElement ; /** Shows pagination footer */ @ bindable . booleanAttr pagination : boolean ; /** Caption for the page size selector */ @ bindable rowsPerPageLabel : string = 'Rows per page' ; /** Page sizes available for selection, e.g. [10, 25, 100, 'All'] */ @ bindable pageSizes : unknown [ ] = [ 10 , 25 , 100 ] ; /** Selected page size */ @ bindable ( { defaultBindingMode : bindingMode . twoWay } ) pageSize : unknown = 10 ; /** Makes header row sticky (fixed) on vertical scroll.
     * (Note: Sticky header feature is not compatible with IE11 browsers.) */ @ bindable . booleanAttr stickyHeader : boolean ; @ computedFrom ( 'pageSize' , 'recordsCount' , 'activePage' ) get paginationPosition ( ) : 'first' | 'between' | 'last' | undefined { if ( typeof this . pageSize !== 'number' || this . pageSize === undefined || isNaN ( this . activePage ) || isNaN ( this . recordsCount ) ) { return undefined ; } const pagesCount = Math . ceil ( this . recordsCount / this . pageSize ) ; return this . activePage === 1 ? ( pagesCount === 1 ? undefined : 'first' ) : ( this . activePage === pagesCount ? 'last' : 'between' ) ; } /** Sets total number of records. Used in navigation row. */ @ bindable . number recordsCount : number ; /** Sets the active page number. Used in navigation row. */ @ bindable . number activePage : number ; @ computedFrom ( 'pageSize' , 'recordsCount' , 'activePage' ) get paginationTotal ( ) : string | undefined { if ( typeof this . pageSize !== 'number' || this . pageSize === undefined || isNaN ( this . activePage ) || isNaN ( this . recordsCount ) ) { return undefined ; } const firstRecord = this . pageSize * ( this . activePage - 1 ) + 1 ; const lastRecord = Math . min ( this . pageSize * this . activePage , this . recordsCount ) ; return `${ firstRecord }-${ lastRecord } of ${ this . recordsCount }` ; } /** Turns on a linear progress indicator at the top of the table */ @ bindable . booleanAttr busy : boolean ; async busyChanged ( ) { await this . initialised ; if ( this . busy ) { this . foundation ?. showProgress ( ) ; } else { this . foundation ?. hideProgress ( ) ; } } @ bindable . booleanAttr hoistPageSelectToBody : boolean ; get rowCheckboxList ( ) : MdcCheckbox [ ] { return Array . from ( this . root . querySelectorAll < IMdcCheckboxElement > ( 'tbody>tr .mdc-checkbox' ) ) . map ( x => x . au ?. controller . viewModel ) ; } get headerRowCheckbox ( ) : MdcCheckbox | undefined { return this . root . querySelector < IMdcCheckboxElement > ( 'thead>tr .mdc-checkbox' ) ?. au . controller . viewModel ; } handleHeaderRowCheckboxChange ( ) { this . foundation ?. handleHeaderRowCheckboxChange ( ) ; } handleRowCheckboxChange ( event ) { if ( ( event . target as HTMLInputElement ) . type === 'checkbox' ) { this . foundation ?. handleRowCheckboxChange ( event ) ; } } headerRowClickListener ( event ) { this . handleHeaderRowClick ( event ) ; } handleNavigationClick ( type ) { this . emit ( NAVIGATION_EVENT , { type } , true ) ; } handleContentClick ( event ) { const dataRowEl = closest ( event . target as Element , selectors . ROW ) as HTMLElement ; if ( ! dataRowEl ) { return ; } this . foundation ?. handleRowClick ( { rowId : this . getRowIdByRowElement ( dataRowEl ) , row : dataRowEl , } ) ; } /**
     * Re-initializes header row checkbox and row checkboxes when selectable rows are added or removed from table.
     */ layout ( ) { this . foundation ?. layout ( ) ; } return getHeaderCells ( ) { return [ ] . slice . call ( this . root . querySelectorAll ( selectors . HEADER_CELL ) ) as Element [ ] ; } return getRows ( ) { return this . foundation ! . getRows ( ) ; } return getSelectedRowIds ( ) { return this . foundation ! . getSelectedRowIds ( ) ; } param rowIds setSelectedRowIds ( rowIds ) { this . foundation ?. setSelectedRowIds ( rowIds ) ; } handleEvent ( evt ) { switch ( evt . currentTarget ) { case this . header : switch ( evt . type ) { case 'change' : this . handleHeaderRowCheckboxChange ( ) ; break ; case 'click' : this . handleHeaderRowClick ( evt ) ; break ; } break ; case this . content : switch ( evt . type ) { case 'change' : this . handleRowCheckboxChange ( evt ) ; break ; } break ; } } async initialise ( ) { this . header = this . root . querySelector < HTMLElement > ( `.${ cssClasses . HEADER_ROW }` ) ! ; this . header . addEventListener ( 'change' , this ) ; this . header . addEventListener ( 'click' , this ) ; this . content = this . root . querySelector < HTMLElement > ( `.${ cssClasses . CONTENT }` ) ! ; this . content . addEventListener ( 'change' , this ) ; this . content . addEventListener ( 'click' , this ) ; const rowCheckboxList = this . rowCheckboxList ; this . rowCheckboxList . forEach ( x => x . root . classList . add ( cssClasses . ROW_CHECKBOX ) ) ; if ( this . headerRowCheckbox ) { this . headerRowCheckbox . root . classList . add ( cssClasses . HEADER_ROW_CHECKBOX ) ; rowCheckboxList . push ( this . headerRowCheckbox ) ; } await Promise . all ( rowCheckboxList . map ( async ( x ) => x . initialised ) ) ; } initialSyncWithDOM ( ) { const rowCheckboxList = this . rowCheckboxList ; for ( let i = 0 ; i < rowCheckboxList . length ; ++ i ) { if ( rowCheckboxList [ i ] . checked ) { this . getRowByIndex ( i ) . classList . add ( cssClasses . ROW_SELECTED ) ; } } this . foundation ?. layout ( ) ; } destroy ( ) { this . header . removeEventListener ( 'change' , this ) ; this . header . removeEventListener ( 'click' , this ) ; this . content . removeEventListener ( 'change' , this ) ; } getDefaultFoundation ( ) { const adapter = { addClass : ( className ) => this . root . classList . add ( className ) , removeClass : ( className ) => this . root . classList . remove ( className ) , getHeaderCellElements : ( ) => this . getHeaderCells ( ) , getHeaderCellCount : ( ) => this . getHeaderCells ( ) . length , getAttributeByHeaderCellIndex : ( index , attribute ) => this . getHeaderCells ( ) [ index ] . getAttribute ( attribute ) , setAttributeByHeaderCellIndex : ( index , attribute , value ) => this . getHeaderCells ( ) [ index ] . setAttribute ( attribute , value ) , setClassNameByHeaderCellIndex : ( index , className ) => this . getHeaderCells ( ) [ index ] . classList . add ( className ) , removeClassNameByHeaderCellIndex : ( index , className ) => { this . getHeaderCells ( ) [ index ] . classList . remove ( className ) ; } , notifySortAction : ( data ) => { this . emit ( events . SORTED , data , true ) ; } , getTableContainerHeight : ( ) => { const tableContainer = this . root . querySelector < HTMLElement > ( `.${ cssClasses . TABLE_CONTAINER }` ) ; if ( ! tableContainer ) { throw new Error ( 'MDCDataTable: Table container element not found.' ) ; } return tableContainer . getBoundingClientRect ( ) . height as any ; } , getTableHeaderHeight : ( ) => { const tableHeader = this . root . querySelector < HTMLElement > ( selectors . HEADER_ROW ) ; if ( ! tableHeader ) { throw new Error ( 'MDCDataTable: Table header element not found.' ) ; } return tableHeader . getBoundingClientRect ( ) . height ; } , setProgressIndicatorStyles : ( styles ) => { const progressIndicator = this . root . querySelector < HTMLElement > ( selectors . PROGRESS_INDICATOR ) ; if ( ! progressIndicator ) { throw new Error ( 'MDCDataTable: Progress indicator element not found.' ) ; } Object . assign ( progressIndicator . style , styles ) ; } , addClassAtRowIndex : ( rowIndex , className ) => { this . getRows ( ) [ rowIndex ] . classList . add ( className ) ; } , getRowCount : ( ) => this . getRows ( ) . length , getRowElements : ( ) => [ ] . slice . call ( this . root . querySelectorAll ( selectors . ROW ) ) as Element [ ] , getRowIdAtIndex : ( rowIndex ) => this . getRows ( ) [ rowIndex ] . getAttribute ( dataAttributes . ROW_ID ) , getRowIndexByChildElement : ( el ) => this . getRows ( ) . indexOf ( ( closest ( el , selectors . ROW ) as HTMLElement ) ) , getSelectedRowCount : ( ) => this . root . querySelectorAll ( selectors . ROW_SELECTED ) . length , isCheckboxAtRowIndexChecked : ( rowIndex ) => this . rowCheckboxList [ rowIndex ] . checked , isHeaderRowCheckboxChecked : ( ) => this . headerRowCheckbox ?. checked ?? false , isRowsSelectable : ( ) => ! ! this . root . querySelector ( selectors . ROW_CHECKBOX ) , notifyRowSelectionChanged : ( data ) => { this . emit ( events . ROW_SELECTION_CHANGED , { row : this . getRowByIndex ( data . rowIndex ) , rowId : this . getRowIdByIndex ( data . rowIndex ) , rowIndex : data . rowIndex , selected : data . selected , } , true ) ; } , notifySelectedAll : ( ) => { this . emit ( events . SELECTED_ALL , { } , true ) ; } , notifyUnselectedAll : ( ) => { this . emit ( events . UNSELECTED_ALL , { } , true ) ; } , notifyRowClick : ( data ) => { this . emit ( events . ROW_CLICK , data , true ) ; } , registerHeaderRowCheckbox : ( ) => { } , registerRowCheckboxes : ( ) => { } , removeClassAtRowIndex : ( rowIndex , className ) => { this . getRows ( ) [ rowIndex ] . classList . remove ( className ) ; } , setAttributeAtRowIndex : ( rowIndex , attr , value ) => { this . getRows ( ) [ rowIndex ] . setAttribute ( attr , value ) ; } , setHeaderRowCheckboxChecked : ( checked ) => { if ( this . headerRowCheckbox ) { this . headerRowCheckbox . checked = checked ; } } , setHeaderRowCheckboxIndeterminate : ( indeterminate ) => { if ( this . headerRowCheckbox ) { this . headerRowCheckbox . indeterminate = indeterminate ; } } , setRowCheckboxCheckedAtIndex : ( rowIndex , checked ) => { this . rowCheckboxList [ rowIndex ] . checked = checked ; } , setSortStatusLabelByHeaderCellIndex : ( columnIndex , sortValue ) => { const headerCell = this . getHeaderCells ( ) [ columnIndex ] ; const sortStatusLabel = headerCell . querySelector < HTMLElement > ( selectors . SORT_STATUS_LABEL ) ; if ( ! sortStatusLabel ) { return ; } sortStatusLabel . textContent = this . getSortStatusMessageBySortValue ( sortValue ) ; } } ; return new MDCDataTableFoundation ( adapter ) ; } private getRowByIndex ( index ) { return this . getRows ( ) [ index ] ; } private getRowIdByIndex ( index ) { return this . getRowByIndex ( index ) . getAttribute ( dataAttributes . ROW_ID ) ; } private handleHeaderRowClick ( event ) { const headerCell = closest ( event . target as Element , selectors . HEADER_CELL_WITH_SORT ) as HTMLElement ; if ( ! headerCell ) { return ; } const columnId = headerCell . getAttribute ( dataAttributes . COLUMN_ID ) ; const columnIndex = this . getHeaderCells ( ) . indexOf ( headerCell ) ; if ( columnIndex === - 1 ) { return ; } this . foundation ?. handleSortAction ( { columnId , columnIndex , headerCell } ) ; } private getSortStatusMessageBySortValue ( sortValue ) { switch ( sortValue ) { case SortValue . ASCENDING : return messages . SORTED_IN_ASCENDING ; case SortValue . DESCENDING : return messages . SORTED_IN_DESCENDING ; default : return '' ; } } private getRowIdByRowElement ( rowElement ) { return rowElement . getAttribute ( dataAttributes . ROW_ID ) ; } }