-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
27 changed files
with
1,510 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import classNames from 'classnames'; | ||
import { Tooltip } from '@/index'; | ||
import * as React from 'react'; | ||
import * as Keys from '@/utils/Keys'; | ||
import { formatPercentage, clamp } from './SliderUtils'; | ||
|
||
export interface HandleProps { | ||
value: number; | ||
fillAfter?: boolean; | ||
fillBefore?: boolean; | ||
onChange?: (newValue: number) => void; | ||
onRelease?: (newValue: number) => void; | ||
} | ||
|
||
export interface InternalHandleProps extends HandleProps { | ||
disabled?: boolean; | ||
label: string; | ||
max: number; | ||
min: number; | ||
stepSize: number; | ||
tickSize: number; | ||
tickSizeRatio: number; | ||
zIndex?: number; | ||
} | ||
|
||
export interface HandleState { | ||
isMoving?: boolean; | ||
} | ||
|
||
export class Handle extends React.Component<InternalHandleProps, HandleState> { | ||
state = { | ||
isMoving: false, | ||
}; | ||
|
||
handleElement: HTMLElement | null = null; | ||
refHandlers = { | ||
handle: (el: HTMLDivElement) => (this.handleElement = el), | ||
}; | ||
|
||
componentWillUnmount() { | ||
this.removeDocumentEventListeners(); | ||
} | ||
|
||
componentDidUpdate(_prevProps: InternalHandleProps, prevState: HandleState) { | ||
if (prevState.isMoving !== this.state.isMoving) { | ||
if (this.handleElement) this.handleElement.focus(); | ||
} | ||
} | ||
|
||
mouseEventClientOffset = (event: MouseEvent | React.MouseEvent<HTMLElement>) => { | ||
return event.clientX; | ||
} | ||
|
||
clientToValue = (clientPixel: number) => { | ||
const { stepSize, tickSize, value } = this.props; | ||
if (this.handleElement == null) { | ||
return value; | ||
} | ||
|
||
const clientPixelNormalized = clientPixel; | ||
const { handleMidpoint, handleOffset } = this.getHandleMidpointAndOffset(this.handleElement); | ||
const handleCenterPixel = handleMidpoint + handleOffset; | ||
const pixelDelta = clientPixelNormalized - handleCenterPixel; | ||
|
||
if (isNaN(pixelDelta)) { | ||
return value; | ||
} | ||
|
||
return value + Math.round(pixelDelta / (tickSize * stepSize)) * stepSize; | ||
} | ||
|
||
changeValue = (newValue: number, callback = this.props.onChange) => { | ||
const updatedValue = clamp(newValue, this.props.min, this.props.max); | ||
|
||
if (!isNaN(updatedValue) && this.props.value !== updatedValue) { | ||
if (callback) callback(updatedValue); | ||
} | ||
return updatedValue; | ||
} | ||
|
||
endHandleMovement = (event: MouseEvent) => { | ||
const clientPixel = this.mouseEventClientOffset(event); | ||
const { onRelease } = this.props; | ||
|
||
this.removeDocumentEventListeners(); | ||
this.setState({ isMoving: false }); | ||
|
||
const finalValue = this.changeValue(this.clientToValue(clientPixel)); | ||
if (onRelease) onRelease(finalValue); | ||
} | ||
|
||
continueHandleMovement = (event: MouseEvent) => { | ||
const clientPixel = this.mouseEventClientOffset(event); | ||
if (this.state.isMoving && !this.props.disabled) { | ||
const value = this.clientToValue(clientPixel); | ||
this.changeValue(value); | ||
} | ||
} | ||
|
||
beginHandleMovement = (event: MouseEvent | React.MouseEvent<HTMLElement>) => { | ||
if (this.props.disabled) return; | ||
document.addEventListener('mousemove', this.continueHandleMovement); | ||
document.addEventListener('mouseup', this.endHandleMovement); | ||
|
||
this.setState({ isMoving: true }); | ||
|
||
const value = this.clientToValue(event.clientX); | ||
this.changeValue(value); | ||
} | ||
|
||
handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => { | ||
if (this.props.disabled) return; | ||
|
||
const { stepSize, value } = this.props; | ||
const { which } = event; | ||
|
||
if (which === Keys.ARROW_LEFT) { | ||
this.changeValue(value - stepSize); | ||
event.preventDefault(); | ||
} else if (which === Keys.ARROW_RIGHT) { | ||
this.changeValue(value + stepSize); | ||
event.preventDefault(); | ||
} | ||
} | ||
|
||
handleKeyUp = (event: React.KeyboardEvent<HTMLSpanElement>) => { | ||
if (this.props.disabled) return; | ||
|
||
if ([Keys.ARROW_LEFT, Keys.ARROW_RIGHT].indexOf(event.which) >= 0) { | ||
const { onRelease } = this.props; | ||
if (onRelease) onRelease(this.props.value); | ||
} | ||
} | ||
|
||
getHandleMidpointAndOffset = (handleElement: HTMLElement | null, useOppositeDimension = false) => { | ||
if (handleElement == null) { | ||
return { handleMidpoint: 0, handleOffset: 0 }; | ||
} | ||
|
||
const handleRect = handleElement.getBoundingClientRect(); | ||
const sizeKey = useOppositeDimension ? 'height' : 'width'; | ||
const handleOffset = handleRect.left; | ||
|
||
return { handleOffset, handleMidpoint: handleRect[sizeKey] / 2 }; | ||
} | ||
|
||
render() { | ||
const { min, tickSizeRatio, value, disabled, label } = this.props; | ||
|
||
const { handleMidpoint } = this.getHandleMidpointAndOffset(this.handleElement, true); | ||
const offsetRatio = (value - min) * tickSizeRatio; | ||
const offsetCalc = `calc(${formatPercentage(offsetRatio)} - ${handleMidpoint}px)`; | ||
const style = { left: offsetCalc }; | ||
|
||
const className = classNames({ | ||
['Slider-handle']: true, | ||
['Slider-handle--disabled']: disabled, | ||
['Slider-handle--active']: this.state.isMoving | ||
}); | ||
|
||
return ( | ||
<div | ||
className={className} | ||
onMouseDown={this.beginHandleMovement} | ||
onKeyDown={this.handleKeyDown} | ||
onKeyUp={this.handleKeyUp} | ||
ref={this.refHandlers.handle} | ||
style={style} | ||
tabIndex={1} | ||
> | ||
{!this.state.isMoving && ( | ||
<Tooltip | ||
tooltip={label} | ||
position="top" | ||
triggerClass={'Slider-tooltip'} | ||
> | ||
<span className="h-100 w-100" /> | ||
</Tooltip> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
removeDocumentEventListeners = () => { | ||
document.removeEventListener('mousemove', this.continueHandleMovement); | ||
document.removeEventListener('mouseup', this.endHandleMovement); | ||
} | ||
} | ||
|
||
export default Handle; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
export const formatPercentage = (ratio: number) => { | ||
return `${(ratio * 100).toFixed(2)}%`; | ||
}; | ||
|
||
export const countDecimalPlaces = (value: number) => { | ||
if (!isFinite(value)) return 0; | ||
|
||
if (Math.floor(value) !== value) { | ||
const valueArray = value.toString().split('.'); | ||
return valueArray[1].length || 0; | ||
} | ||
|
||
return 0; | ||
}; | ||
|
||
export const approxEqual = (a: number, b: number) => { | ||
const tolerance = 0.00001; | ||
return Math.abs(a - b) <= tolerance; | ||
}; | ||
|
||
export const clamp = (value: number, min: number, max: number) => { | ||
if (value == null) { | ||
return value; | ||
} | ||
|
||
return Math.min(Math.max(value, min), max); | ||
}; | ||
|
||
export const arraysEqual = (oldValues: number[], newValues: number[]) => { | ||
|
||
if (oldValues.length !== oldValues.length) return; | ||
|
||
return newValues.every((value, index) => value === oldValues[index]); | ||
}; | ||
|
||
export function argMin<T>(values: T[], argFn: (value: T) => any): T | undefined { | ||
if (values.length === 0) { | ||
return undefined; | ||
} | ||
|
||
let minValue = values[0]; | ||
let minArg = argFn(minValue); | ||
|
||
for (let index = 1; index < values.length; index++) { | ||
const value = values[index]; | ||
const arg = argFn(value); | ||
if (arg < minArg) { | ||
minValue = value; | ||
minArg = arg; | ||
} | ||
} | ||
|
||
return minValue; | ||
} | ||
|
||
export function fillValues<T>(values: T[], startIndex: number, endIndex: number, fillValue: T) { | ||
const inc = startIndex < endIndex ? 1 : -1; | ||
for (let index = startIndex; index !== endIndex + inc; index += inc) { | ||
values[index] = fillValue; | ||
} | ||
|
||
} | ||
|
||
export function isElementOfType<P = {}>( | ||
element: any, | ||
_ComponentType: React.ComponentType<P>, | ||
): element is React.ReactElement<P> { | ||
return ( | ||
element != null && | ||
element.type != null | ||
); | ||
} |
Oops, something went wrong.