import { LanguageOptions , Theme } from '@vscodethemes/types' ; import { cx } from 'emotion' ; import * as React from 'react' ; import { Heading , ThemePreview } from '../' ; import theme from '../../theme' ; import NextButton from './NextButton' ; import PreviousButton from './PreviousButton' ; import styles , { sizes } from './ThemeSlider.styles' ; interface ThemeSliderProps { title : string ; description : string ; themes : Array < Theme | undefined > ; language : LanguageOptions ; onLanguage : ( language : LanguageOptions ) => any ; moreProps ? : any ; } interface ThemeSliderState { currentIndex : number ; queuedIndex : number ; numOfVisibleItems : number ; itemWidthPercent : number ; shouldPeak : boolean ; didPeak : boolean ; } class ThemeSlider extends React . Component < ThemeSliderProps , ThemeSliderState > { state = { numOfVisibleItems : 6 , currentIndex : 0 , queuedIndex : 0 , itemWidthPercent : 0 , shouldPeak : false , didPeak : false , } ; rowEl : HTMLElement ; itemEl : HTMLElement ; mqListeners : Array < { mql : MediaQueryList ; listener : MediaQueryListListener ; } > ; componentDidMount ( ) { this . calculateVisibleItems ( ) ; this . mqListeners = sizes . map ( size => { const mql = window . matchMedia ( size . media ) ; const listener = e => { if ( e . matches ) { this . setState ( { currentIndex : 0 , queuedIndex : 0 } ) ; this . calculateVisibleItems ( ) ; } } ; mql . addListener ( listener ) ; return { mql , listener } ; } ) ; } componentWillUnmount ( ) { this . mqListeners . forEach ( ( { mql , listener } ) => { mql . removeListener ( listener ) ; } ) ; this . mqListeners = [ ] ; } shouldComponentUpdate ( nextProps , nextState ) { return ( nextProps . language !== this . props . language || nextState . currentIndex !== this . state . currentIndex || nextState . queuedIndex !== this . state . queuedIndex || nextState . shouldPeak !== this . state . shouldPeak || nextState . didPeak !== this . state . didPeak ) ; } rowRef = ( el ) => { this . rowEl = el ; } ; itemRef = ( el ) => { this . itemEl = el ; } ; calculateVisibleItems = ( ) => { const rowRect = this . rowEl . getBoundingClientRect ( ) ; const itemRect = this . itemEl . getBoundingClientRect ( ) ; const numOfVisibleItems = Math . floor ( rowRect . width / itemRect . width ) ; const itemWidthPercent = Math . round ( ( itemRect . width / rowRect . width ) * 100 ) ; this . setState ( { numOfVisibleItems , itemWidthPercent } ) ; } ; nextSlide = ( ) => { const { currentIndex , numOfVisibleItems } = this . state ; this . setState ( { queuedIndex : currentIndex + numOfVisibleItems , } ) ; } ; previousSlide = ( ) => { const { currentIndex , numOfVisibleItems } = this . state ; this . setState ( { queuedIndex : currentIndex - numOfVisibleItems , } ) ; } ; handleSlideEnd = ( ) => { const { currentIndex , queuedIndex , didPeak } = this . state ; if ( queuedIndex !== currentIndex ) { this . setState ( { currentIndex : queuedIndex } ) ; } if ( currentIndex === 0 ) { this . setState ( { shouldPeak : false } ) ; } if ( didPeak ) { this . setState ( { didPeak : false } ) ; } } ; peak = ( ) => { this . setState ( { shouldPeak : true , didPeak : true } ) ; } ; unpeak = ( ) => { this . setState ( { shouldPeak : false , didPeak : true } ) ; } ; render ( ) { const { title , description , themes , language , onLanguage , moreProps , } = this . props ; const { currentIndex , queuedIndex , numOfVisibleItems , itemWidthPercent , shouldPeak , didPeak , } = this . state ; const maxIndex = Math . floor ( themes . length / numOfVisibleItems ) * numOfVisibleItems - numOfVisibleItems ; const themesToRender = [ ] ; const lowerIndex = currentIndex - numOfVisibleItems - 1 ; const upperIndex = currentIndex + numOfVisibleItems * 2 ; let renderIndex = - 1 ; let themeIndex = lowerIndex ; for ( ; themeIndex <= upperIndex ; themeIndex += 1 ) { if ( themes [ themeIndex ] && themeIndex < maxIndex ) { if ( currentIndex === themeIndex ) { renderIndex = themesToRender . length ; } themesToRender . push ( themes [ themeIndex ] ) ; } } const isAtRest = queuedIndex === currentIndex ; const slideDelta = isAtRest ? 0 : queuedIndex - currentIndex ; let distance = - itemWidthPercent * ( renderIndex + slideDelta ) ; let duration = isAtRest ? 0 : 0.5 ; const firstIndex = 0 ; const isFirstSlide = currentIndex === firstIndex || queuedIndex === firstIndex ; const lastIndex = maxIndex - numOfVisibleItems ; const isLastSlide = currentIndex === lastIndex || queuedIndex === lastIndex ; if ( ! isFirstSlide && shouldPeak ) { distance += itemWidthPercent / 4 ; } if ( didPeak ) { duration = 0.25 ; } const rowStyles = { transform : `translateX(${ distance }%)` , transition : duration > 0 ? `transform ${ duration }s ${ theme . animation . bezier }` : '' , } ; return ( < div className = { styles . wrapper } > < div className = { styles . title } > < Heading text = { title } /> < /div> < div className = { styles . clip } > < div ref = { this . rowRef } className = { styles . row } style = { rowStyles } onTransitionEnd = { this . handleSlideEnd } > { themesToRender . map ( ( t , i ) => ( < div ref = { i === 0 && this . itemRef } key = { t . themeId } className = { styles . item } > < ThemePreview theme = { t } language = { language } onLanguage = { onLanguage } / > < /div>))} { isLastSlide && ( < a { ... moreProps } className = { styles . more } > View more < br / > < span style = { { whiteSpace : 'nowrap' } } > { description } < /span> < /a>)} < /div> < div className = { cx ( styles . previous , shouldPeak && styles . previousExpanded ) } onMouseOver = { isFirstSlide ? null : this . peak } onMouseLeave = { isFirstSlide ? null : this . unpeak } > < PreviousButton onClick = { this . previousSlide } hide = { isFirstSlide } / > < /div> < NextButton onClick = { this . nextSlide } hide = { isLastSlide } / > < /div> < /div>); } } export default ThemeSlider ;