license docs-private import { Direction } from '@angular/cdk/bidi' ; export type StickyDirection = 'top' | 'bottom' | 'left' | 'right' ; docs-private export const STICKY_DIRECTIONS = [ 'top' , 'bottom' , 'left' , 'right' ] ; docs-private export class StickyStyler { param isNativeHtmlTable param stickCellCss param direction param _isBrowser constructor ( private isNativeHtmlTable , private stickCellCss , public direction , private _isBrowser = true ) { } param rows param stickyDirections clearStickyPositioning ( rows , stickyDirections ) { for ( const row of rows ) { if ( row . nodeType !== row . ELEMENT_NODE ) { continue ; } this . _removeStickyStyle ( row , stickyDirections ) ; for ( let i = 0 ; i < row . children . length ; i ++ ) { const cell = row . children [ i ] as HTMLElement ; this . _removeStickyStyle ( cell , stickyDirections ) ; } } } param rows param stickyStartStates param stickyEndStates updateStickyColumns ( rows , stickyStartStates , stickyEndStates ) { const hasStickyColumns = stickyStartStates . some ( state => state ) || stickyEndStates . some ( state => state ) ; if ( ! rows . length || ! hasStickyColumns || ! this . _isBrowser ) { return ; } const firstRow = rows [ 0 ] ; const numCells = firstRow . children . length ; const cellWidths = this . _getCellWidths ( firstRow ) ; const startPositions = this . _getStickyStartColumnPositions ( cellWidths , stickyStartStates ) ; const endPositions = this . _getStickyEndColumnPositions ( cellWidths , stickyEndStates ) ; const isRtl = this . direction === 'rtl' ; for ( const row of rows ) { for ( let i = 0 ; i < numCells ; i ++ ) { const cell = row . children [ i ] as HTMLElement ; if ( stickyStartStates [ i ] ) { this . _addStickyStyle ( cell , isRtl ? 'right' : 'left' , startPositions [ i ] ) ; } if ( stickyEndStates [ i ] ) { this . _addStickyStyle ( cell , isRtl ? 'left' : 'right' , endPositions [ i ] ) ; } } } } param rowsToStick param stickyStates param position stickRows ( rowsToStick , stickyStates , position ) { if ( ! this . _isBrowser ) { return ; } const rows = position === 'bottom' ? rowsToStick . reverse ( ) : rowsToStick ; let stickyHeight = 0 ; for ( let rowIndex = 0 ; rowIndex < rows . length ; rowIndex ++ ) { if ( ! stickyStates [ rowIndex ] ) { continue ; } const row = rows [ rowIndex ] ; if ( this . isNativeHtmlTable ) { for ( let j = 0 ; j < row . children . length ; j ++ ) { const cell = row . children [ j ] as HTMLElement ; this . _addStickyStyle ( cell , position , stickyHeight ) ; } } else { this . _addStickyStyle ( row , position , stickyHeight ) ; } if ( rowIndex === rows . length - 1 ) { return ; } stickyHeight += row . getBoundingClientRect ( ) . height ; } } /**
     * When using the native table in Safari, sticky footer cells do not stick. The only way to stick
     * footer rows is to apply sticky styling to the tfoot container. This should only be done if
     * all footer rows are sticky. If not all footer rows are sticky, remove sticky positioning from
     * the tfoot element.
     */ updateStickyFooterContainer ( tableElement , stickyStates ) { if ( ! this . isNativeHtmlTable ) { return ; } const tfoot = tableElement . querySelector ( 'tfoot' ) ! ; if ( stickyStates . some ( state => ! state ) ) { this . _removeStickyStyle ( tfoot , [ 'bottom' ] ) ; } else { this . _addStickyStyle ( tfoot , 'bottom' , 0 ) ; } } /**
     * Removes the sticky style on the element by removing the sticky cell CSS class, re-evaluating
     * the zIndex, removing each of the provided sticky directions, and removing the
     * sticky position if there are no more directions.
     */ _removeStickyStyle ( element , stickyDirections ) { for ( const dir of stickyDirections ) { element . style [ dir ] = '' ; } element . style . zIndex = this . _getCalculatedZIndex ( element ) ; const hasDirection = STICKY_DIRECTIONS . some ( dir => ! ! element . style [ dir ] ) ; if ( ! hasDirection ) { element . style . position = '' ; element . classList . remove ( this . stickCellCss ) ; } } /**
     * Adds the sticky styling to the element by adding the sticky style class, changing position
     * to be sticky (and -webkit-sticky), setting the appropriate zIndex, and adding a sticky
     * direction and value.
     */ _addStickyStyle ( element , dir , dirValue ) { element . classList . add ( this . stickCellCss ) ; element . style [ dir ] = `${ dirValue }px` ; element . style . cssText += 'position: -webkit-sticky; position: sticky; ' ; element . style . zIndex = this . _getCalculatedZIndex ( element ) ; } /**
     * Calculate what the z-index should be for the element, depending on what directions (top,
     * bottom, left, right) have been set. It should be true that elements with a top direction
     * should have the highest index since these are elements like a table header. If any of those
     * elements are also sticky in another direction, then they should appear above other elements
     * that are only sticky top (e.g. a sticky column on a sticky header). Bottom-sticky elements
     * (e.g. footer rows) should then be next in the ordering such that they are below the header
     * but above any non-sticky elements. Finally, left/right sticky elements (e.g. sticky columns)
     * should minimally increment so that they are above non-sticky elements but below top and bottom
     * elements.
     */ _getCalculatedZIndex ( element ) { const zIndexIncrements = { top : 100 , bottom : 10 , left : 1 , right : 1 , } ; let zIndex = 0 ; for ( const dir of STICKY_DIRECTIONS ) { if ( element . style [ dir ] ) { zIndex += zIndexIncrements [ dir ] ; } } return zIndex ? `${ zIndex }` : '' ; } /** Gets the widths for each cell in the provided row. */ _getCellWidths ( row ) { const cellWidths = [ ] ; const firstRowCells = row . children ; for ( let i = 0 ; i < firstRowCells . length ; i ++ ) { let cell = firstRowCells [ i ] as HTMLElement ; cellWidths . push ( cell . getBoundingClientRect ( ) . width ) ; } return cellWidths ; } /**
     * Determines the left and right positions of each sticky column cell, which will be the
     * accumulation of all sticky column cell widths to the left and right, respectively.
     * Non-sticky cells do not need to have a value set since their positions will not be applied.
     */ _getStickyStartColumnPositions ( widths , stickyStates ) { const positions = [ ] ; let nextPosition = 0 ; for ( let i = 0 ; i < widths . length ; i ++ ) { if ( stickyStates [ i ] ) { positions [ i ] = nextPosition ; nextPosition += widths [ i ] ; } } return positions ; } /**
     * Determines the left and right positions of each sticky column cell, which will be the
     * accumulation of all sticky column cell widths to the left and right, respectively.
     * Non-sticky cells do not need to have a value set since their positions will not be applied.
     */ _getStickyEndColumnPositions ( widths , stickyStates ) { const positions = [ ] ; let nextPosition = 0 ; for ( let i = widths . length ; i > 0 ; i -- ) { if ( stickyStates [ i ] ) { positions [ i ] = nextPosition ; nextPosition += widths [ i ] ; } } return positions ; } }