<div id="root"></div>
const { useState, useRef, useCallback, useEffect } = React;
const { createGlobalStyle, css } = styled;
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Orelega+One&display=swap');
:root {
--accent-color: rgb(62, 93, 228);
--text-color: #333;
--bg-color: #CFDBEA;
--button-color: #333;
--button-text-color: #fff;
--button-disabled-color: #ccc;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orelega One', cursive;
font-size: 4vmin;
margin: 0;
height: 100vh;
width: 100%;
}
`;
const Spacer = ({ size, horizontal }) => {
return (
<div
style={
horizontal
? { width: size, height: 'auto', display: 'inline-block', flexShrink: 0 }
: { width: 'auto', height: size, flexShrink: 0 }
}
/>
)
}
const StyledTimeDisplay = styled.div`
color: ${({ textColor }) => textColor ? textColor : `#333`};
font-weight: bold;
font-size: ${({ fontSize }) => fontSize ? fontSize : '1em'};
`;
const TimeDisplay = ({ className, time, delimiter, fontSize, textColor }) => {
const newTime = Array.isArray(time) ? time : Object.values(time)
return (
<StyledTimeDisplay
className={className}
fontSize={fontSize}
textColor={textColor}
>
{
newTime.map((n, i, array) => (
<>
<span>{String(n).padStart(2, "0")}</span>
{(i !== array.length - 1) && delimiter}
</>
))
}
</StyledTimeDisplay>
);
};
const StyledWheelPicker = styled.div`
display: grid;
grid-template-rows: repeat(2, auto);
${({ itemLength, delimiterLength }) => `
grid-template-columns: repeat(${itemLength + delimiterLength}, auto);
`}// = auto-fill
place-items: center;
align-items: center;
justify-content: center;
gap: .5em;
& .delimiter {
font-size: .75em;
&-1 {
grid-column: 2 / 3;
}
&-2 {
grid-column: 4 / 5;
}
&-3 {
grid-column: 6 / 7;
}
}
& .dial {
&-display {
border-top: .1em solid #333;
border-bottom: .1em solid #333;
height: 1em;
overflow-y: hidden;
padding: .4em;
&-1 {
grid-column: 1 / 2;
}
&-2 {
grid-column: 3 / 4;
}
&-3 {
grid-column: 5 / 6;
}
}
&-title {
font-size: .6em;
&-1 {
grid-column: 1 / 2;
}
&-2 {
grid-column: 3 / 4;
}
&-3 {
grid-column: 5 / 6;
}
}
&-nums {
cursor: pointer;
position: relative;
top: 0em;
transition: .2s linear;
user-select: none;
}
&-num {
height: 1em;
}
}
`
const WheelPicker = ({ className, items, delimiters, handleWheel, handleDown }) => {
const itemsArray = Object.keys(items)//Object.entries(items).map((t) => t.shift())
/*useEffect(() => {
console.log('items', Object.entries(items).map((t) => t.shift()))
}, [items])*/
return (
<StyledWheelPicker
className={className}
itemLength={items.length}
delimiterLength={delimiters.length}
>
{
itemsArray.map((item, i, arr) => (
<div className={`dial-title dial-title-${i + 1}`}>
{item}
</div>
))
}
{
itemsArray.map((item, i, arr) => (
<>
<div className={`dial-display dial-display-${i + 1}`}>
<div
className={`dial-nums dial-nums-${i}
draggable`}
key={i}
id={item}
onMouseDown={handleDown}
onWheel={handleWheel}
>
{
items[item].map((elem, i, array) => (
<div className="dial-num" key={i}>{String(elem).padStart(2, "0")}</div>
))
}
</div>
</div>
{delimiters[i] && <div className={`delimiter delimiter-${i + 1}`}>{delimiters[i]}</div>}
</>
))
}
<Spacer size=".5em" horizontal={true} />
</StyledWheelPicker>
)
}
/*const useDragElement = () => {
const [dragAmount, setDragAmount] = useState({ x: 0, y: 0 });
const [elementPosition, setElementPosition] = useState({ top: 0, left: 0 });
const startPoint = useRef({ x: 0, y: 0 });
const draggingElement = useRef(null);
const verticalDirection = useRef(null);
const horizontalDirection = useRef(null);
const prevPosition = useRef({ x: 0, y: 0 });
const handleDown = useCallback((e) => {
draggingElement.current = e.currentTarget;
//const rect = draggingElement.current.getBoundingClientRect();
//const top = rect.top;
//const left = rect.left;
const x = e.clientX - draggingElement.current.offsetLeft;//left;
const y = e.clientY - draggingElement.current.offsetTop;//top;
startPoint.current = { x, y };
});
const handleMove = useCallback((e) => {
if (!draggingElement.current) return;
const x = e.clientX - startPoint.current.x;
const y = e.clientY - startPoint.current.y;
if (x > prevPosition.current.x) {
horizontalDirection.current = "right";
}
if(x < prevPosition.current.x) {
horizontalDirection.current = "left";
}
if (y > prevPosition.current.y) {
verticalDirection.current = "bottom";
}
if (y < prevPosition.current.y) {
verticalDirection.current = "top";
}
setDragAmount({ x, y });
//draggingElement.current.style.transform = `translate3d(${x}px, ${y}px, 0)`
prevPosition.current = {
x,
y
};
});
const handleUp = useCallback((e) => {
if (!draggingElement.current) return;
setElementPosition((p) => ({
top: dragAmount.y,
left: dragAmount.x
}));
//draggingElement.current.style.top = `${dragAmount.y}px`
//draggingElement.current.style.left = `${dragAmount.x}px`
draggingElement.current = null;
console.log('up')
});
useEffect(() => {
document.body.addEventListener("mousemove", handleMove);
document.body.addEventListener("mouseup", handleUp);
document.body.addEventListener("mouseleave", handleUp);
return () => {
document.body.removeEventListener("mousemove", handleMove);
document.body.removeEventListener("mouseup", handleUp);
document.body.removeEventListener("mouseleave", handleUp);
};
}, []);
return [
draggingElement.current,
dragAmount,
elementPosition,
verticalDirection.current,
horizontalDirection.current,
handleDown
]
}*/
const useDraggableElements = () => {
const [draggableElements, setDraggableElements] = useState([])
const startPoint = useRef({ x: 0, y: 0 });
const draggingElement = useRef(null);
const draggingDirection = useRef({
horizontal: null,
vertical: null,
})
const handleDown = useCallback((e) => {
draggingElement.current = e.currentTarget;
const draggableElements = document.getElementsByClassName("draggable");
for(let i = 0; i < draggableElements.length; i++) {
draggableElements[i].style.zIndex = `1000`
}
draggingElement.current.style.zIndex = `1001`
setDraggableElements(currentDraggableElements => {
currentDraggableElements.forEach(elm => {
if(draggingElement.current.className.includes(elm.name)) {
const x = e.pageX - elm.x
const y = e.pageY - elm.y
startPoint.current = { x, y };
}
})
return currentDraggableElements
})
console.log(startPoint.current)
});
const handleMove = useCallback((e) => {
e.preventDefault();
if (!draggingElement.current) return;
const x = e.pageX - startPoint.current.x;
const y = e.pageY - startPoint.current.y;
setDraggableElements(currentDraggableElements => {
currentDraggableElements.forEach(elm => {
if(draggingElement.current.className.includes(elm.name)){
if (x > elm.x) {
draggingDirection.current.horizontal = "right";
}
if (x < elm.x) {
draggingDirection.current.horizontal = "left";
}
if (y > elm.y) {
draggingDirection.current.vertical = "bottom";
}
if (y < elm.y) {
draggingDirection.current.vertical = "top";
}
}
})
return currentDraggableElements
})
draggingElement.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;
setDraggableElements(currentDraggableElements =>
currentDraggableElements.map(elm =>
draggingElement.current.className.includes(elm.name) ? ({
...elm,
x,
y
}) : elm))
});
const handleUp = useCallback((e) => {
if (!draggingElement.current) return;
draggingElement.current = null;// nullにしないと、先頭でif (!draggingElement.current) return;としているため、要素がカーソルから離れない
console.log("up");
});
// 初回のレンダー後に一度だけ行う処理
useEffect(() => {
const draggableElements = document.getElementsByClassName("draggable");
for(let i = 0; i < draggableElements.length; i++) {
draggableElements[i].addEventListener("mousedown", handleDown);
}
document.body.addEventListener("mousemove", handleMove);
document.body.addEventListener("mouseup", handleUp);
document.body.addEventListener("mouseleave", handleUp);
setDraggableElements(currentDraggableElements => {
const newDraggableElements = [...currentDraggableElements]
for(let i = 0; i < draggableElements.length; i++) {
newDraggableElements[i] = {
name: draggableElements[i].className,
x: 0,
y: 0
}
}
return newDraggableElements
})
return () => {
document.body.removeEventListener("mousemove", handleMove);
document.body.removeEventListener("mouseup", handleUp);
document.body.removeEventListener("mouseleave", handleUp);
};
}, [])
// ドラッグしている方向
useEffect(() => {
console.log(draggingDirection.current);
}, [draggableElements]);
return [
draggingElement.current,
draggableElements,
draggingDirection.current
];
};
const useWheelPicker = (nums) => {
const [selectNum, setSelectNum] = useState(() => {
const newNums = {}
Object.keys(nums).forEach((key) => {
newNums[key] = nums[key][0]
})
return newNums
})
/*const [
draggingElement,
dragAmount,
elementPosition,
verticalDirection,
horizontalDirection,
handleDown
] = useDragElement()*/
const [
draggingElement,
draggableElements,
draggingDirection
] = useDraggableElements();
const handleWheel = useCallback((e) => {
const deltaY = e.deltaY;
const elm = e.currentTarget;
const targetNums = nums[elm.id]
setSelectNum(p => {
const currentSelectNum = Math.abs(p[elm.id])
const currentSelectNumIndex = targetNums.indexOf(currentSelectNum)
const firstIndex = 0
const lastIndex = targetNums.length - 1
console.log('currentSelectNum', currentSelectNum)
console.log('currentSelectNumIndex', currentSelectNumIndex)
const firstNum = targetNums[firstIndex]
const lastNum = targetNums[lastIndex]
if (e.deltaY > firstIndex) {// 下にホイール
const nextIndex = currentSelectNumIndex + 1
const nextNum = targetNums[currentSelectNumIndex + 1];
console.log('nextNum', nextNum)
if(nextIndex > lastIndex){
elm.style.transform = `translate3d(0, ${firstIndex}em, 0)`
return {
...p,
[elm.id]: firstNum
}
}else{
elm.style.transform = `translate3d(0, -${nextIndex}em, 0)`
return {
...p,
[elm.id]: nextNum
}
}
}else{// 上にホイール
const prevIndex = currentSelectNumIndex - 1
const prevNum = targetNums[currentSelectNumIndex - 1];
if(prevIndex < firstIndex) {
elm.style.transform = `translate3d(0, -${lastIndex}em, 0)`
return {
...p,
[elm.id]: lastNum
}
}else{
elm.style.transform = `translate3d(0, -${prevIndex}em, 0)`
return {
...p,
[elm.id]: prevNum
}
}
}
})
});
useEffect(() => {
if(!draggingElement) return;
//if(Math.abs(dragAmount.y) % 8 !== 0) return
//console.log('dragAmount.y', dragAmount.y)
console.log('draggingElement', draggingElement)
const targetNums = nums[draggingElement.id]
setSelectNum(p => {
const currentSelectNum = Math.abs(p[draggingElement.id])
const currentSelectNumIndex = targetNums.indexOf(currentSelectNum)
const firstIndex = 0
const lastIndex = targetNums.length - 1
console.log('currentSelectNum', currentSelectNum)
console.log('currentSelectNumIndex', currentSelectNumIndex)
const firstNum = targetNums[firstIndex]
const lastNum = targetNums[lastIndex]
if(draggingDirection.vertical === 'top') {
// 上へドラッグ
const prevIndex = currentSelectNumIndex - 1
const prevNum = targetNums[currentSelectNumIndex - 1];
if(prevIndex < firstIndex) {
draggingElement.style.transform = `translate3d(0, -${lastIndex}em, 0)`
return {
...p,
[draggingElement.id]: lastNum
}
}else{
draggingElement.style.transform = `translate3d(0, -${prevIndex}em, 0)`
return {
...p,
[draggingElement.id]: prevNum
}
}
}
if(draggingDirection.vertical === 'bottom') {
// 下へドラッグ
const nextIndex = currentSelectNumIndex + 1
const nextNum = targetNums[currentSelectNumIndex + 1];
if(nextIndex > lastIndex) {
draggingElement.style.transform = `translate3d(0, -${firstIndex}em, 0)`//`0em`
return {
...p,
[draggingElement.id]: firstNum
}
}else{
draggingElement.style.transform = `translate3d(0, -${nextIndex}em, 0)`
return {
...p,
[draggingElement.id]: nextNum
}
}
}
})
console.log('isVerticalDirection', draggingDirection.vertical)
}, [draggableElements])
return [
selectNum,
handleWheel
]
}
const date = new Date()
const year = date.getFullYear()
const DATE = {
Days: [...Array(31)].map((u, i) => i + 1),
Months: [...Array(12)].map((u, i) => i + 1),
Years: [...Array(120)].map((u, i) => year - i),
}
// 6th Janauary 2008
const DELIMITERS = ['/', '/']
const StyledApp = styled.div`
width: 16em;
`
const App = () => {
const [
selectNum,
//handleDown,
handleWheel
] = useWheelPicker(DATE)
return (
<>
<GlobalStyle />
<StyledApp>
<WheelPicker
className="wheel-picker"
items={DATE}
delimiters={DELIMITERS}
handleWheel={handleWheel}
//handleDown={handleDown}
/>
<Spacer size="1.4em" />
<TimeDisplay
className="date-of-birth"
time={selectNum}
delimiter="/"
fontSize="2.8em"
/>
</StyledApp>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
This Pen doesn't use any external CSS resources.