import classNames from "classnames" ; import * as React from "react" ; import { polyfill } from "react-lifecycles-compat" ; import { AbstractPureComponent2 , Classes , Intent } from "../../common" ; import * as Errors from "../../common/errors" ; import { DISPLAYNAME_PREFIX , IntentProps , Props } from "../../common/props" ; import * as Utils from "../../common/utils" ; import { Handle } from "./handle" ; import { HandleInteractionKind , HandleType , HandleProps } from "./handleProps" ; import { argMin , fillValues , formatPercentage } from "./sliderUtils" ; /**
 * SFC used to pass slider handle props to a `MultiSlider`.
 * This element is not rendered directly.
 */ const MultiSliderHandle = ( ) => null ; MultiSliderHandle . displayName = `${ DISPLAYNAME_PREFIX }.MultiSliderHandle` ; export interface ISliderBaseProps extends Props , IntentProps { default disabled ? : boolean ; default labelStepSize ? : number ; /**
     * Array of specific values for the label placement. This prop is mutually exclusive with
     * `labelStepSize`.
     */ labelValues ? : number [ ] ; default labelPrecision ? : number ; default max ? : number ; default min ? : number ; default showTrackFill ? : boolean ; default stepSize ? : number ; default labelRenderer ? : boolean | ( ( value : number , opts ? : { isHandleTooltip : boolean ; } ) => string | JSX . Element ) ; default vertical ? : boolean ; } export type MultiSliderProps = IMultiSliderProps ; deprecated export interface IMultiSliderProps extends ISliderBaseProps { /** Default intent of a track segment, used only if no handle specifies `intentBefore/After`. */ defaultTrackIntent ? : Intent ; /** Callback invoked when a handle value changes. Receives handle values in sorted order. */ onChange ? ( values ) : void ; /** Callback invoked when a handle is released. Receives handle values in sorted order. */ onRelease ? ( values ) : void ; } export interface ISliderState { labelPrecision : number ; /** the client size, in pixels, of one tick */ tickSize : number ; /** the size of one tick as a ratio of the component's client size */ tickSizeRatio : number ; } @ polyfill export class MultiSlider extends AbstractPureComponent2 < MultiSliderProps , ISliderState > { public static defaultSliderProps : ISliderBaseProps = { disabled : false , max : 10 , min : 0 , showTrackFill : true , stepSize : 1 , vertical : false , } ; public static defaultProps : MultiSliderProps = { ... MultiSlider . defaultSliderProps , defaultTrackIntent : Intent . NONE , } ; public static displayName = `${ DISPLAYNAME_PREFIX }.MultiSlider` ; public static Handle = MultiSliderHandle ; public static getDerivedStateFromProps ( props ) { return { labelPrecision : MultiSlider . getLabelPrecision ( props ) } ; } private static getLabelPrecision ( { labelPrecision , stepSize } ) { return labelPrecision == null ? Utils . countDecimalPlaces ( stepSize ! ) : labelPrecision ; } public state : ISliderState = { labelPrecision : getLabelPrecision ( this . props ) , tickSize : 0 , tickSizeRatio : 0 , } ; private handleElements : Handle [ ] = [ ] ; private trackElement : HTMLElement | null = null ; public getSnapshotBeforeUpdate ( prevProps ) { const prevHandleProps = getSortedInteractiveHandleProps ( prevProps ) ; const newHandleProps = getSortedInteractiveHandleProps ( this . props ) ; if ( newHandleProps . length !== prevHandleProps . length ) { this . handleElements = [ ] ; } return null ; } public render ( ) { const classes = classNames ( Classes . SLIDER , { [ Classes . DISABLED ] : this . props . disabled , [ `${ Classes . SLIDER }-unlabeled` ] : this . props . labelRenderer === false , [ Classes . VERTICAL ] : this . props . vertical , } , this . props . className ) ; return ( < div className = { classes } onMouseDown = { this . maybeHandleTrackClick } onTouchStart = { this . maybeHandleTrackTouch } > < div className = { Classes . SLIDER_TRACK } ref = { ref => ( this . trackElement = ref ) } > { this . renderTracks ( ) } < /div> < div className = { Classes . SLIDER_AXIS } > { this . renderLabels ( ) } < /div> { this . renderHandles ( ) } < /div>); } public componentDidMount ( ) { this . updateTickSize ( ) ; } public componentDidUpdate ( prevProps , prevState ) { super . componentDidUpdate ( prevProps , prevState ) ; this . updateTickSize ( ) ; } protected validateProps ( props ) { if ( props . stepSize ! <= 0 ) { throw new Error ( Errors . SLIDER_ZERO_STEP ) ; } if ( props . labelStepSize !== undefined && props . labelValues !== undefined ) { throw new Error ( Errors . MULTISLIDER_WARN_LABEL_STEP_SIZE_LABEL_VALUES_MUTEX ) ; } if ( props . labelStepSize !== undefined && props . labelStepSize ! <= 0 ) { throw new Error ( Errors . SLIDER_ZERO_LABEL_STEP ) ; } let anyInvalidChildren = false ; React . Children . forEach ( props . children , child => { if ( child && ! Utils . isElementOfType ( child , MultiSlider . Handle ) ) { anyInvalidChildren = true ; } } ) ; if ( anyInvalidChildren ) { throw new Error ( Errors . MULTISLIDER_INVALID_CHILD ) ; } } private formatLabel ( value , isHandleTooltip = false ) { const { labelRenderer } = this . props ; if ( labelRenderer === false ) { return undefined ; } else if ( Utils . isFunction ( labelRenderer ) ) { return labelRenderer ( value , { isHandleTooltip } ) ; } else { return value . toFixed ( this . state . labelPrecision ) ; } } private renderLabels ( ) { if ( this . props . labelRenderer === false ) { return null ; } const values = this . getLabelValues ( ) ; const { max , min } = this . props ; const labels = values . map ( ( step , i ) => { const offsetPercentage = formatPercentage ( ( step - min ! ) / ( max ! - min ! ) ) ; const style = this . props . vertical ? { bottom : offsetPercentage } : { left : offsetPercentage } ; return ( < div className = { Classes . SLIDER_LABEL } key = { i } style = { style } > { this . formatLabel ( step ) } < /div>); } ) ; return labels ; } private renderTracks ( ) { const trackStops = getSortedHandleProps ( this . props ) ; trackStops . push ( { value : this . props . max ! } ) ; let previous = { value : this . props . min ! } ; const handles = [ ] ; for ( let index = 0 ; index < trackStops . length ; index ++ ) { const current = trackStops [ index ] ; handles . push ( this . renderTrackFill ( index , previous , current ) ) ; previous = current ; } return handles ; } private renderTrackFill ( index , start , end ) { const [ startRatio , endRatio ] = [ this . getOffsetRatio ( start . value ) , this . getOffsetRatio ( end . value ) ] . sort ( ( left , right ) => left - right ) ; const startOffset = formatPercentage ( startRatio ) ; const endOffset = formatPercentage ( 1 - endRatio ) ; const orientationStyle = this . props . vertical ? { bottom : startOffset , top : endOffset , left : 0 } : { left : startOffset , right : endOffset , top : 0 } ; const style = { ... orientationStyle , ... ( start . trackStyleAfter || end . trackStyleBefore || { } ) , } ; const classes = classNames ( Classes . SLIDER_PROGRESS , Classes . intentClass ( this . getTrackIntent ( start , end ) ) ) ; return < div key = { `track-${ index }` } className = { classes } style = { style } / > ; } private renderHandles ( ) { const { disabled , max , min , stepSize , vertical } = this . props ; const handleProps = getSortedInteractiveHandleProps ( this . props ) ; if ( handleProps . length === 0 ) { return null ; } return handleProps . map ( ( { value , type , className } , index ) => ( < Handle className = { classNames ( { [ Classes . START ] : type === HandleType . START , [ Classes . END ] : type === HandleType . END , } , className ) } disabled = { disabled } key = { `${ index }-${ handleProps . length }` } label = { this . formatLabel ( value , true ) } max = { max ! } min = { min ! } onChange = { this . getHandlerForIndex ( index , this . handleChange ) } onRelease = { this . getHandlerForIndex ( index , this . handleRelease ) } ref = { this . addHandleRef } stepSize = { stepSize ! } tickSize = { this . state . tickSize } tickSizeRatio = { this . state . tickSizeRatio } value = { value } vertical = { vertical ! } / > ) ) ; } private addHandleRef = ( ref ) => { if ( ref != null ) { this . handleElements . push ( ref ) ; } } ; private maybeHandleTrackClick = ( event ) => { if ( this . canHandleTrackEvent ( event ) ) { const foundHandle = this . nearestHandleForValue ( this . handleElements , handle => handle . mouseEventClientOffset ( event ) ) ; if ( foundHandle ) { foundHandle . beginHandleMovement ( event ) ; } } } ; private maybeHandleTrackTouch = ( event ) => { if ( this . canHandleTrackEvent ( event ) ) { const foundHandle = this . nearestHandleForValue ( this . handleElements , handle => handle . touchEventClientOffset ( event ) ) ; if ( foundHandle ) { foundHandle . beginHandleTouchMovement ( event ) ; } } } ; private canHandleTrackEvent = ( event ) => { const target = event . target as HTMLElement ; return ! this . props . disabled && target . closest ( `.${ Classes . SLIDER_HANDLE }` ) == null ; } ; private nearestHandleForValue ( handles , getOffset ) { return argMin ( handles , handle => { const offset = getOffset ( handle ) ; const offsetValue = handle . clientToValue ( offset ) ; const handleValue = handle . props . value ! ; return Math . abs ( offsetValue - handleValue ) ; } ) ; } private getHandlerForIndex = ( index , callback ? ) => { return ( newValue ) => { callback ?. ( this . getNewHandleValues ( newValue , index ) ) ; } ; } ; private getNewHandleValues ( newValue , oldIndex ) { const handleProps = getSortedInteractiveHandleProps ( this . props ) ; const oldValues = handleProps . map ( handle => handle . value ) ; const newValues = oldValues . slice ( ) ; newValues [ oldIndex ] = newValue ; newValues . sort ( ( left , right ) => left - right ) ; const newIndex = newValues . indexOf ( newValue ) ; const lockIndex = this . findFirstLockedHandleIndex ( oldIndex , newIndex ) ; if ( lockIndex === - 1 ) { fillValues ( newValues , oldIndex , newIndex , newValue ) ; } else { const lockValue = oldValues [ lockIndex ] ; fillValues ( oldValues , oldIndex , lockIndex , lockValue ) ; return oldValues ; } return newValues ; } private findFirstLockedHandleIndex ( startIndex , endIndex ) { const inc = startIndex < endIndex ? 1 : - 1 ; const handleProps = getSortedInteractiveHandleProps ( this . props ) ; for ( let index = startIndex + inc ; index !== endIndex + inc ; index += inc ) { if ( handleProps [ index ] . interactionKind !== HandleInteractionKind . PUSH ) { return index ; } } return - 1 ; } private handleChange = ( newValues ) => { const handleProps = getSortedInteractiveHandleProps ( this . props ) ; const oldValues = handleProps . map ( handle => handle . value ) ; if ( ! Utils . arraysEqual ( newValues , oldValues ) ) { this . props . onChange ?. ( newValues ) ; handleProps . forEach ( ( handle , index ) => { if ( oldValues [ index ] !== newValues [ index ] ) { handle . onChange ?. ( newValues [ index ] ) ; } } ) ; } } ; private handleRelease = ( newValues ) => { const handleProps = getSortedInteractiveHandleProps ( this . props ) ; this . props . onRelease ?. ( newValues ) ; handleProps . forEach ( ( handle , index ) => { handle . onRelease ?. ( newValues [ index ] ) ; } ) ; } ; private getLabelValues ( ) { const { labelStepSize , labelValues , min , max } = this . props ; let values = [ ] ; if ( labelValues !== undefined ) { values = labelValues ; } else { for ( let i = min ! ; i < max ! || Utils . approxEqual ( i , max ! ) ; i += labelStepSize ?? 1 ) { values . push ( i ) ; } } return values ; } private getOffsetRatio ( value ) { return Utils . clamp ( ( value - this . props . min ! ) * this . state . tickSizeRatio , 0 , 1 ) ; } private getTrackIntent ( start , end ? ) { if ( ! this . props . showTrackFill ) { return Intent . NONE ; } if ( start . intentAfter !== undefined ) { return start . intentAfter ; } else if ( end !== undefined && end . intentBefore !== undefined ) { return end . intentBefore ; } return this . props . defaultTrackIntent ! ; } private updateTickSize ( ) { if ( this . trackElement != null ) { const trackSize = this . props . vertical ? this . trackElement . clientHeight : this . trackElement . clientWidth ; const tickSizeRatio = 1 / ( ( this . props . max as number ) - ( this . props . min as number ) ) ; const tickSize = trackSize * tickSizeRatio ; this . setState ( { tickSize , tickSizeRatio } ) ; } } } function getLabelPrecision ( { labelPrecision , stepSize = MultiSlider . defaultSliderProps . stepSize ! } ) { return labelPrecision == null ? Utils . countDecimalPlaces ( stepSize ) : labelPrecision ; } function getSortedInteractiveHandleProps ( props ) { return getSortedHandleProps ( props , childProps => childProps . interactionKind !== HandleInteractionKind . NONE ) ; } function getSortedHandleProps ( { children } , predicate = ( ) => true ) { const maybeHandles = React . Children . map ( children , child => Utils . isElementOfType ( child , MultiSlider . Handle ) && predicate ( child . props ) ? child . props : null ) ; let handles = maybeHandles != null ? maybeHandles : [ ] ; handles = handles . filter ( handle => handle !== null ) ; handles . sort ( ( left , right ) => left . value - right . value ) ; return handles ; }