I haven't found any answer to this yet. The accepted answer is not really an answer but it points to a github library. I am going to try to include here a complete answer using only react.
Here it goes, the code should be self explanatory, but a couple of words ahead of time. We need to use a lot of state variables to keep state between renders, otherwise any variables get reset out. To make the transitions smooth, I update the position once a render was completed using useEffect hook. I tested this in codesandbox, I'm including the link here for anybody to edit the code and play with it, just fork it. It workd with the MS Surface Book2 Pro
and Android. It has a formatting problem with the iPhone IOS. Both for Safari and Chrome. If somebody fixes it that'd be great. For now I have what I need and claim success.
Here are the files under src in codesandbox.io:
App.js
import "./styles/index.pcss";
import "./styles/tailwind-pre-build.css";
import Photos from "./Photos.js";
export default function App() {
return (
<>
<div className="flow-root bg-green-200">
<div className="my-4 bg-blue-100 mb-20">
Drag and Drop with touch screens
</div>
</div>
<div className="flow-root bg-red-200">
<div className="bg-blue-100">
<Photos />
</div>
</div>
</>
);
}
Photos.js:
import React, { useState } from "react";
import "./styles/index.pcss";
import Image from "./image";
export default function Photos() {
const [styleForNumber, setStyleForNumber] = useState({
position: "relative",
width: "58px",
height: "58px"
});
const photosArray = [
"https://spinelli.io/noderestshop/uploads/G.1natalie.1642116451444",
"https://spinelli.io/noderestshop/uploads/G.2natalie.1642116452437",
"https://spinelli.io/noderestshop/uploads/G.3natalie.1642116453418",
"https://spinelli.io/noderestshop/uploads/G.4natalie.1642116454396",
"https://spinelli.io/noderestshop/uploads/G.5natalie.1642116455384",
"https://spinelli.io/noderestshop/uploads/G.6natalie.1642116456410",
"https://spinelli.io/noderestshop/uploads/G.7natalie.1642116457466",
"https://spinelli.io/noderestshop/uploads/G.8natalie.1642116458535",
"https://spinelli.io/noderestshop/uploads/G.0natalie.1642116228246"
];
return (
<>
<div
className="w-1/2 bg-green-200"
style={{
display: "grid",
gridTemplateColumns: "[first] 60px [second] 60px [third] 60px",
gridTemplateRows: "60px 60px 60px",
rowGap: "10px",
columnGap: "20px",
position: "relative",
justifyContent: "center",
placeItems: "center"
}}
>
{photosArray.map((photo, i) => (
<div
className="relative z-1 h-full w-full flex flex-wrap content-center touch-none"
key={i}
>
<div className="contents">
<Image photo={photo} i={i} />
</div>
</div>
))}
</div>
</>
);
}
Image.js:
import React, { useRef, useState, useEffect } from "react";
import "./styles/index.pcss";
export default function Image({ photo, i }) {
const imgRef = useRef();
const [top, setTop] = useState(0);
const [left, setLeft] = useState(0);
const [drag, setDrag] = useState(false);
const [styleForImg, setStyleForImg] = useState({
position: "absolute",
width: "58px",
height: "58px"
});
const [offsetTop, setOffsetTop] = useState(-40);
const [offsetLeft, setOffsetLeft] = useState(0);
const [xAtTouchPointStart, setXAtTouchPointStart] = useState(0);
const [yAtTouchPointStart, setYAtTouchPointStart] = useState(0);
useEffect(() => {
if (drag) {
setStyleForImg({
position: "relative",
width: "58px",
height: "58px",
top: top,
left: left
});
} else {
setStyleForImg({
position: "relative",
width: "58px",
height: "58px"
});
}
console.log("style: ", styleForImg);
}, [drag, top, left]);
const handleTouchStart = (e, i) => {
e.preventDefault();
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
const x = +touch.pageX;
const y = +touch.pageY;
console.log(
"onTouchStart coordinates of icon @ start: X: " + x + " | Y: " + y
);
console.log("dragged from position n = ", i + 1);
// get the mouse cursor position at startup:
setXAtTouchPointStart(x);
setYAtTouchPointStart(y);
setDrag(true);
};
const handleTouchEnd = (e) => {
// if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
e.preventDefault();
setDrag(false);
console.log(
new Date(),
"onTouchEnd event, coordinates of icon @ end: X: " +
e.changedTouches[0]?.clientX +
" | Y: " +
e.changedTouches[0]?.clientY +
" | top: " +
top +
" | left: " +
left
);
};
const handleElementDrag = (e) => {
e = e || window.event;
e.preventDefault();
let x = 0;
let y = 0;
//Get touch or click position
//https://stackoverflow.com/a/41993300/5078983
if (
e.type === "touchstart" ||
e.type === "touchmove" ||
e.type === "touchend" ||
e.type === "touchcancel"
) {
let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0];
x = +touch.pageX; // X Coordinate relative to the viewport of the touch point
y = +touch.pageY; // same for Y
} else if (
e.type === "mousedown" ||
e.type === "mouseup" ||
e.type === "mousemove" ||
e.type === "mouseover" ||
e.type === "mouseout" ||
e.type === "mouseenter" ||
e.type === "mouseleave"
) {
x = +e.clientX;
y = +e.clientY;
}
console.log("x: ", x, "y: ", y);
// calculate the new cursor position:
const xRelativeToStart = x - xAtTouchPointStart;
console.log(
"xRel = ",
x,
" - ",
xAtTouchPointStart,
" = ",
xRelativeToStart
);
const yRelativeToStart = y - yAtTouchPointStart;
console.log(
"yRel = ",
y,
" - ",
yAtTouchPointStart,
" = ",
yRelativeToStart
);
// setXAtTouchPointStart(x); // Reseting relative point to current touch point
// setYAtTouchPointStart(y);
// set the element's new position:
setTop(yRelativeToStart + "px");
setLeft(xRelativeToStart + "px");
console.log("top: ", yRelativeToStart + "px");
console.log("Left: ", xRelativeToStart + "px");
};
const handleDragEnd = (e) => {
// if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
console.log(
new Date(),
"Coordinates of icon @ end X: " + e.clientX + " | Y: " + e.clientY
);
};
const handleDragStart = (e, i) => {
// From https://stackoverflow.com/a/69109382/15355839
e.stopPropagation(); // let child take the drag
e.dataTransfer.dropEffect = "move";
e.dataTransfer.effectAllowed = "move";
console.log(
"Coordinates of icon @ start: X: " + e.clientX + " | Y: " + e.clientY
);
// console.log ('event: ', e)
console.log("dragged from position n = ", i + 1);
};
return (
<img
ref={imgRef}
className="hover:border-none border-4 border-solid border-green-600 mb-4"
src={photo}
alt="placeholder"
style={styleForImg}
onDragStart={(e) => handleDragStart(e, i)}
onDragEnd={handleDragEnd}
onTouchStart={(e) => handleTouchStart(e, i)}
onTouchEnd={handleTouchEnd}
onTouchMove={handleElementDrag}
></img>
);
}
index.js:
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./styles/index.pcss";
import App from "./App";
const root = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
root
);
styles.css:
.Main {
font-family: sans-serif;
text-align: center;
}
/styles/index.pcss:
@tailwind base;
@tailwind components;
@tailwind utilities;
I couldn't make tailwinds grid work, so I used the actual css inline styles. No idea why they didn't in codesandbox.