react-material-time-picker
Version:
TimePicker is a user interface component that allows the user to easily select a specific time. It provides an analog clock interface that is easy to use and intuitive. TimePicker can be easily integrated into other user interface components, making it a
299 lines (225 loc) • 10.2 kB
JavaScript
import React, {useEffect, useMemo, useRef, useState,useCallback} from "react";
import {Clock} from "./Clock.js";
import {decode,normalize} from "../../utilities.js";
import"./analog.scss"
import {useTheme} from "../../ThemeContext.js";
export default function Analog(props) {
const clockFace = useRef();
const hand = useRef();
let animationId = 0;
const clockPos = clockFace.current?.getBoundingClientRect() || { left:0, top:0 };
const pointerFace = useRef();
const digitsClockFace = useRef();
const hoursClockFace = useMemo(()=>{ return new Clock()},[]);
const minutesClockFace = useMemo(()=>{ return new Clock()},[]);
const [ hoursDigits, setHoursDigits ] = useState([]);
const [ minutesDigits, setMinutesDigits ] = useState([]);
const [ hoursPointerDigit,setHoursPointerDigit ] = useState( 0);
const [ minutesPointerDigit,setMinutesPointerDigit ] = useState( 0);
const [ radius,setRadius ] = useState(128);
const pageIsLoaded = useRef(false);
const [ colors ] = useTheme();
const digits = props.mode === 'hours' ? hoursDigits : minutesDigits;
const pointer = props.mode === 'hours' ? hoursPointerDigit :minutesPointerDigit;
const setPointer = props.mode === 'hours' ? setHoursPointerDigit : setMinutesPointerDigit;
let clock = props.mode === 'hours' ? hoursClockFace : minutesClockFace;
const angelToPos= useCallback( radian =>{
return {
x: Math.round( Math.cos( radian )* radius),
y: Math.round( Math.sin( radian )* radius)
}
},[radius])
const relocatePointerByAngel=useCallback( (angel,pointPlacement=null,value=null) => {
const degree = radToDegree( angel );
if( pointPlacement === null ) pointPlacement = angelToPos( angel,radius );
if( value === null) value = pointer;
pointerFace.current.style.transform = "translate("+ pointPlacement.x+"px,"+ pointPlacement.y+"px)";
pointerFace.current.innerHTML = props.mode === 'hours'
? value === 0 ? 12 : value
: value;
hand.current.style.transform = "rotate("+degree+"deg)"
},[angelToPos,pointer,props.mode,radius])
const relocatePointerByIndex = useCallback( idx =>{
if( digits.length === 0) return;
let point = digits[ idx ].placement;
const rad = clock.angel( idx );
relocatePointerByAngel( rad,point,idx )
},[clock,digits,relocatePointerByAngel])
const shortestPath= useCallback( (start,dest) => {
let path;
let difference = dest - start;
let distance;
const fullClock = (props.mode === 'minutes' ? 60 : 12)
if( ( difference > 0 && difference < fullClock/2 ) || fullClock - start + dest <= fullClock/2) {
distance = difference > 0 ? difference : fullClock - start + dest;
path = clock.goClockwise( start, distance )
} else {
distance = Math.abs (dest - start) >= fullClock/2 ? fullClock - dest+ start : start - dest;
path = clock.goCounterClockwise( start, distance )
}
return path;
},[props.mode,clock])
const handleAnimatedRelocation = useCallback( destIdx =>{
const start = props.mode === 'hours' ? pointer%12 : pointer;
const path = shortestPath( start , destIdx );
const waitTime = 300/path.length
function wait(ms) {
return new Promise(resolve => setTimeout(resolve , ms))
}
async function loop(counter) {
if(counter >= path.length){
setPointer( destIdx );
return;
}
await relocatePointerByIndex(path[ counter ].value);
await wait(waitTime);
requestAnimationFrame(()=>{ loop(counter+1)})
}
loop(0);
},[pointer,props.mode,relocatePointerByIndex,setPointer,shortestPath])
const drawClock = useCallback( (radius,offset)=> {
hoursClockFace.draw( radius , 12);
minutesClockFace.draw( radius , 60);
setHoursDigits( hoursClockFace.getDigits() );
setMinutesDigits( minutesClockFace.getDigits() );
const clockFaceElement = digitsClockFace.current;
const pointerElement = pointerFace.current;
pointerElement.style.width = offset * 3+'px';
pointerElement.style.height = offset * 3+'px';
pointerElement.style.top = radius - offset+'px';
pointerElement.style.left = radius - offset+'px';
clockFaceElement.style.transform = 'translate('+radius+'px,'+radius+'px)'
},[hoursClockFace,minutesClockFace])
function radToDegree (rad) {
return rad *180 /Math.PI;
}
function getAngel (x,y) {
const rad = Math.atan2( y,x );
return Math.round(rad * 100 )/ 100
}
function getAngelByIndex ( idx ) {
const numberOfUnits = props.mode === 'hours' ? 12 : 60;
if ( idx >= 0 && idx < numberOfUnits ){
return getAngel( digits[idx].placement.x, digits[idx].placement.y);
}
}
function handleDrag (e) {
let mousePos;
let angel = getAngelByIndex( pointer) //the last position of the hand clock
function handleRelease() {
window.removeEventListener('mousemove',startAnimation);
window.removeEventListener('mouseup',handleRelease);
let closestDigit = clock.getTheClosestDigit(angel);
setPointer( closestDigit );
setGlobalTime( closestDigit )
relocatePointerByIndex( closestDigit )
}
function startAnimation(e){
if( animationId ) cancelAnimationFrame( animationId );
animationId = requestAnimationFrame(() => trackMouse(e));
}
function trackMouse (e) {
mousePos = getPosFromClockCenter( e.clientX,e.clientY );
angel = getAngel( mousePos.x,mousePos.y );
relocatePointerByAngel( angel,null, clock.getTheClosestDigit(angel) )
}
e.preventDefault();
window.addEventListener( "mousemove", startAnimation );
window.addEventListener( 'mouseup', handleRelease );
}
function getPosFromClockCenter( xPosFromDoc,yPosFromDoc ) {
let posFromParent={};
posFromParent.x = xPosFromDoc - clockPos.left - radius;
posFromParent.y = yPosFromDoc - clockPos.top - radius;
return posFromParent;
}
function handleAutoRelocate( index ) {
handleAnimatedRelocation( index );
setGlobalTime( index );
}
function setGlobalTime( newTime ) {
let time = decode( props.time );
( props.mode === 'hours'
? props.onChange( `${ props.dayMode === 'pm'
? parseInt( newTime ) +12
: normalize( newTime ) }${ time.minute }`)
: props.onChange( `${ time.hour }${ normalize( newTime ) }`)
)
}
const getGlobalTime=useCallback( mode =>{
const time = decode( props.time );
return mode === 'hours'
? parseInt( time.hour%12 )
: parseInt( time.minute );
},[props.time])
useEffect(()=>{
if( typeof props.time !== 'undefined' && digits.length >0 ) {
const digit = getGlobalTime( props.mode )
if( digit !== pointer ){
handleAnimatedRelocation( digit );
}
}
},[ props.time,props.mode, digits.length,getGlobalTime,handleAnimatedRelocation,pointer])
useEffect(()=>{
if( typeof clockFace.current !== 'undefined') {
const diameter = clockFace.current?.getBoundingClientRect().width;
const offset = parseFloat( window.getComputedStyle( clockFace.current, null ).getPropertyValue('font-size' ));
const radius = Math.round(diameter- offset - 2*offset/( diameter/4 ))/2;
setRadius( radius );
drawClock( radius, offset);
}
},[ clockFace,drawClock ])
useEffect(()=>{
if( digits.length > 0 && pageIsLoaded.current === false ) {
relocatePointerByIndex( getGlobalTime( props.mode ))
pageIsLoaded.current = true;
}
},[ digits,pageIsLoaded,getGlobalTime,relocatePointerByIndex,props.mode ])
useEffect(()=>{
relocatePointerByIndex(pointer)
},[ pointer,relocatePointerByIndex ])
return (
<div ref={ clockFace }
className='clock'
style={{
background:colors.surfaceVariant ,
color: colors.onSurfaceVariant,
width: props.clockWidth+'px'
}}
>
<div className='center'
style={{ backgroundColor:colors.primary }}
/>
<div className='digits-clockFace' ref={digitsClockFace}>
{ Object.values( digits ).length > 0
? Object.values( digits ).map(( point,index)=>(
<span key={ index }
id={ index.toString()}
onClick={ ()=> handleAutoRelocate( index ) }
style= {{ transform: "translate("+
point.placement.x +"px,"+
point.placement.y +"px)"
}}>
{ props.mode === 'hours'
? index === 0 ? 12 : index
: index%5 === 0 ? index :<span className='no-show-minutes'>.</span>
}
</span>
))
: ''
}
</div>
<div
ref={ hand }
className='hand'
style={{ background:`linear-gradient( 250deg, ${ colors.primary } 50%, #AC6BFF00 0%)`}}
/>
<div ref={ pointerFace }
onMouseDown={ handleDrag }
className='pointer'
style={{ backgroundColor:colors.primary,
color:colors.onPrimary
}}
/>
</div>)
}