import classNames from "classnames" ; import * as React from "react" ; import { polyfill } from "react-lifecycles-compat" ; import { AbstractPureComponent2 , Classes , Keys } from "../../common" ; import { DISPLAYNAME_PREFIX } from "../../common/props" ; import { clamp } from "../../common/utils" ; import { HandleProps } from "./handleProps" ; import { formatPercentage } from "./sliderUtils" ; /**
 * Props for the internal <Handle> component needs some additional info from the parent Slider.
 */ export interface IInternalHandleProps extends HandleProps { disabled ? : boolean ; label : JSX . Element | string | undefined ; max : number ; min : number ; stepSize : number ; tickSize : number ; tickSizeRatio : number ; vertical : boolean ; } export interface IHandleState { /** whether slider handle is currently being dragged */ isMoving ? : boolean ; } const NUMBER_PROPS = [ "max" , "min" , "stepSize" , "tickSize" , "value" ] ; /** Internal component for a Handle with click/drag/keyboard logic to determine a new value. */ @ polyfill export class Handle extends AbstractPureComponent2 < IInternalHandleProps , IHandleState > { public static displayName = `${ DISPLAYNAME_PREFIX }.SliderHandle` ; public state = { isMoving : false , } ; private handleElement : HTMLElement | null = null ; private refHandlers = { handle : ( el ) => ( this . handleElement = el ) , } ; public componentDidMount ( ) { this . forceUpdate ( ) ; } public render ( ) { const { className , disabled , label } = this . props ; const { isMoving } = this . state ; return ( < span className = { classNames ( Classes . SLIDER_HANDLE , { [ Classes . ACTIVE ] : isMoving } , className ) } onKeyDown = { disabled ? undefined : this . handleKeyDown } onKeyUp = { disabled ? undefined : this . handleKeyUp } onMouseDown = { disabled ? undefined : this . beginHandleMovement } onTouchStart = { disabled ? undefined : this . beginHandleTouchMovement } ref = { this . refHandlers . handle } style = { this . getStyleProperties ( ) } tabIndex = { 0 } > { label == null ? null : < span className = { Classes . SLIDER_LABEL } > { label } < /span>} < /span>); } public componentWillUnmount ( ) { this . removeDocumentEventListeners ( ) ; } public clientToValue ( clientPixel ) { const { stepSize , tickSize , value , vertical } = this . props ; if ( this . handleElement == null ) { return value ; } const clientPixelNormalized = vertical ? window . innerHeight - clientPixel : clientPixel ; const handleCenterPixel = this . getHandleElementCenterPixel ( this . handleElement ) ; const pixelDelta = clientPixelNormalized - handleCenterPixel ; if ( isNaN ( pixelDelta ) ) { return value ; } return value + Math . round ( pixelDelta / ( tickSize * stepSize ) ) * stepSize ; } public mouseEventClientOffset ( event ) { return this . props . vertical ? event . clientY : event . clientX ; } public touchEventClientOffset ( event ) { const touch = event . changedTouches [ 0 ] ; return this . props . vertical ? touch . clientY : touch . clientX ; } public beginHandleMovement = ( event ) => { document . addEventListener ( "mousemove" , this . handleHandleMovement ) ; document . addEventListener ( "mouseup" , this . endHandleMovement ) ; this . setState ( { isMoving : true } ) ; this . changeValue ( this . clientToValue ( this . mouseEventClientOffset ( event ) ) ) ; } ; public beginHandleTouchMovement = ( event ) => { document . addEventListener ( "touchmove" , this . handleHandleTouchMovement ) ; document . addEventListener ( "touchend" , this . endHandleTouchMovement ) ; document . addEventListener ( "touchcancel" , this . endHandleTouchMovement ) ; this . setState ( { isMoving : true } ) ; this . changeValue ( this . clientToValue ( this . touchEventClientOffset ( event ) ) ) ; } ; protected validateProps ( props ) { for ( const prop of NUMBER_PROPS ) { if ( typeof ( props as any ) [ prop ] !== "number" ) { throw new Error ( `[Blueprint] <Handle> requires number value for ${ prop } prop` ) ; } } } private getStyleProperties = ( ) : React . CSSProperties => { if ( this . handleElement == null ) { return { } ; } const { min = 0 , tickSizeRatio , value , vertical } = this . props ; const { handleMidpoint } = this . getHandleMidpointAndOffset ( this . handleElement , true ) ; const offsetRatio = ( value - min ) * tickSizeRatio ; const offsetCalc = `calc(${ formatPercentage ( offsetRatio ) } - ${ handleMidpoint }px)` ; return vertical ? { bottom : offsetCalc } : { left : offsetCalc } ; } ; private endHandleMovement = ( event ) => { this . handleMoveEndedAt ( this . mouseEventClientOffset ( event ) ) ; } ; private endHandleTouchMovement = ( event ) => { this . handleMoveEndedAt ( this . touchEventClientOffset ( event ) ) ; } ; private handleMoveEndedAt = ( clientPixel ) => { this . removeDocumentEventListeners ( ) ; this . setState ( { isMoving : false } ) ; const finalValue = this . changeValue ( this . clientToValue ( clientPixel ) ) ; this . props . onRelease ?. ( finalValue ) ; } ; private handleHandleMovement = ( event ) => { this . handleMovedTo ( this . mouseEventClientOffset ( event ) ) ; } ; private handleHandleTouchMovement = ( event ) => { this . handleMovedTo ( this . touchEventClientOffset ( event ) ) ; } ; private handleMovedTo = ( clientPixel ) => { if ( this . state . isMoving && ! this . props . disabled ) { this . changeValue ( this . clientToValue ( clientPixel ) ) ; } } ; private handleKeyDown = ( event ) => { const { stepSize , value } = this . props ; const { which } = event ; if ( which === Keys . ARROW_DOWN || which === Keys . ARROW_LEFT ) { this . changeValue ( value - stepSize ) ; event . preventDefault ( ) ; } else if ( which === Keys . ARROW_UP || which === Keys . ARROW_RIGHT ) { this . changeValue ( value + stepSize ) ; event . preventDefault ( ) ; } } ; private handleKeyUp = ( event ) => { if ( [ Keys . ARROW_UP , Keys . ARROW_DOWN , Keys . ARROW_LEFT , Keys . ARROW_RIGHT ] . indexOf ( event . which ) >= 0 ) { this . props . onRelease ?. ( this . props . value ) ; } } ; private changeValue ( newValue , callback = this . props . onChange ) { newValue = this . clamp ( newValue ) ; if ( ! isNaN ( newValue ) && this . props . value !== newValue ) { callback ?. ( newValue ) ; } return newValue ; } private clamp ( value ) { return clamp ( value , this . props . min , this . props . max ) ; } private getHandleElementCenterPixel ( handleElement ) { const { handleMidpoint , handleOffset } = this . getHandleMidpointAndOffset ( handleElement ) ; return handleOffset + handleMidpoint ; } private getHandleMidpointAndOffset ( handleElement , useOppositeDimension = false ) { if ( handleElement == null ) { return { handleMidpoint : 0 , handleOffset : 0 } ; } const { vertical } = this . props ; const handleRect = handleElement . getBoundingClientRect ( ) ; const sizeKey = vertical ? useOppositeDimension ? "width" : "height" : useOppositeDimension ? "height" : "width" ; const handleOffset = vertical ? window . innerHeight - ( handleRect . top + handleRect [ sizeKey ] ) : handleRect . left ; return { handleMidpoint : handleRect [ sizeKey ] / 2 , handleOffset } ; } private removeDocumentEventListeners ( ) { document . removeEventListener ( "mousemove" , this . handleHandleMovement ) ; document . removeEventListener ( "mouseup" , this . endHandleMovement ) ; document . removeEventListener ( "touchmove" , this . handleHandleTouchMovement ) ; document . removeEventListener ( "touchend" , this . endHandleTouchMovement ) ; document . removeEventListener ( "touchcancel" , this . endHandleTouchMovement ) ; } }