Skip to content

Commit

Permalink
improve drag and drop w/ auto scroll at border
Browse files Browse the repository at this point in the history
  • Loading branch information
morris committed Dec 31, 2024
1 parent 93e0766 commit f4f2dea
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 60 deletions.
1 change: 0 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"extends": "stylelint-config-standard",
"rules": {
"property-no-vendor-prefix": null,
"selector-class-pattern": "^[\\-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$"
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,8 @@ Thanks!

- Add dark mode
- Add Playwright config for testing more browsers
- Scroll automatically when dragging items at the window border
- Improve drag and drop behavior on touch devices
- Update dependencies

### 08/2024
Expand Down
5 changes: 4 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0"
/>
<meta name="theme-color" content="#fefefe" />

<title>VANILLA TODO</title>
Expand Down
121 changes: 73 additions & 48 deletions public/scripts/AppDraggable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,35 @@
* dropSelector: string;
* dragThreshold?: number;
* dropRange?: number;
* scrollThreshold?: number;
* scrollSpeed?: number;
* }} options
*/
export function AppDraggable(el, options) {
const dragThreshold = options.dragThreshold ?? 5;
const dropRange = options.dropRange ?? 50;
const dropRangeSquared = dropRange * dropRange;
const scrollThreshold = options.scrollThreshold ?? 12;
const scrollSpeed = options.scrollSpeed ?? 7;

let originX, originY;
let clientX, clientY;
let startTime;
let dragging = false;
let clicked = false;
let data;
let image;
let imageSource;
let imageX, imageY;
let currentTarget;

el.addEventListener('touchstart', start, { passive: true });
el.addEventListener('mousedown', start);
el.addEventListener('mousedown', start, { passive: true });

// Maybe prevent click
// Prevent click while dragging
el.addEventListener(
'click',
(e) => {
if (dragging || clicked) {
if (dragging) {
e.preventDefault();
e.stopImmediatePropagation();
}
Expand All @@ -42,8 +45,6 @@ export function AppDraggable(el, options) {
if (e.type === 'mousedown' && e.button !== 0) return;
if (e.touches && e.touches.length > 1) return;

e.preventDefault();

const p = getPositionHost(e);
clientX = originX = p.clientX ?? p.pageX;
clientY = originY = p.clientY ?? p.pageY;
Expand All @@ -53,8 +54,6 @@ export function AppDraggable(el, options) {
}

function move(e) {
e.preventDefault();

const p = getPositionHost(e);
clientX = p.clientX ?? p.pageX;
clientY = p.clientY ?? p.pageY;
Expand All @@ -72,7 +71,7 @@ export function AppDraggable(el, options) {
return;
}

// prevent unintentional dragging on touch devices
// Prevent unintentional dragging on touch devices
if (e.touches && Date.now() - startTime < 50) {
stopListening();
return;
Expand All @@ -85,33 +84,27 @@ export function AppDraggable(el, options) {
dispatchDrag();
dispatchTarget();
dispatchOverContinuously();
autoScroll();
}

function end(e) {
e.preventDefault();

if (!dragging) {
e.target.click();
clicked = true;
}

function end() {
stopListening();

requestAnimationFrame(() => {
clicked = false;

if (dragging) {
dispatchTarget();
dispatchEnd();

dragging = false;
}
});
}

function startListening() {
el.addEventListener('touchmove', move);
el.addEventListener('touchend', end);
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', end);
el.addEventListener('touchmove', move, { passive: true });
el.addEventListener('touchend', end, { passive: true });
window.addEventListener('mousemove', move, { passive: true });
window.addEventListener('mouseup', end, { passive: true });
}

function stopListening() {
Expand Down Expand Up @@ -144,8 +137,6 @@ export function AppDraggable(el, options) {
}

function dispatchTarget() {
if (!dragging) return;

const nextTarget = getTarget();

if (nextTarget === currentTarget) return;
Expand Down Expand Up @@ -176,11 +167,6 @@ export function AppDraggable(el, options) {
function dispatchOverContinuously() {
if (!dragging) return;

dispatchOver();
setTimeout(dispatchOver, 50);
}

function dispatchOver() {
if (currentTarget) {
currentTarget.dispatchEvent(
new CustomEvent('draggableOver', {
Expand All @@ -190,7 +176,7 @@ export function AppDraggable(el, options) {
);
}

setTimeout(dispatchOver, 50);
setTimeout(dispatchOverContinuously, 50);
}

function dispatchEnd() {
Expand All @@ -212,6 +198,35 @@ export function AppDraggable(el, options) {
}
}

function autoScroll() {
if (!dragging) return;

let x = 0;
let y = 0;

if (clientX < scrollThreshold) {
if (window.scrollX > 0) {
x = -1;
}
} else if (clientX > window.innerWidth - scrollThreshold) {
x = 1;
}

if (clientY < scrollThreshold) {
if (window.scrollY > 0) {
y = -1;
}
} else if (clientY > window.innerHeight - scrollThreshold) {
y = 1;
}

if (x !== 0 || y !== 0) {
window.scrollBy(x * scrollSpeed, y * scrollSpeed);
}

requestAnimationFrame(autoScroll);
}

//

function buildDetail() {
Expand Down Expand Up @@ -312,43 +327,53 @@ export function AppDraggable(el, options) {

candidates.push({
el,
distance2: distanceSquared,
distanceSquared,
});
});

candidates.sort((a, b) => {
if (a.distance2 === 0 && b.distance2 === 0) {
if (a.distanceSquared === 0 && b.distanceSquared === 0) {
// in this case, the client position is inside both rectangles
// if A contains B, B is the correct target and vice versa
// TODO sort by z-index somehow?
return a.el.contains(b.el) ? -1 : b.el.contains(a.el) ? 1 : 0;
}

// sort by distance, ascending
return a.distance2 - b.distance2;
return a.distanceSquared - b.distanceSquared;
});

return candidates.length > 0 ? candidates[0].el : null;
}
}

function pointDistanceToRectSquared(x, y, rect) {
const dx =
x < rect.left ? x - rect.left : x > rect.right ? x - rect.right : 0;
const dy =
y < rect.top ? y - rect.top : y > rect.bottom ? y - rect.bottom : 0;
export function pointDistanceToRectSquared(x, y, rect) {
let dx = 0;
let dy = 0;

return dx * dx + dy * dy;
if (x < rect.left) {
dx = x - rect.left;
} else if (x > rect.right) {
dx = x - rect.right;
}

function getPositionHost(e) {
if (e.targetTouches && e.targetTouches.length > 0) {
return e.targetTouches[0];
}
if (y < rect.top) {
dy = y - rect.top;
} else if (y > rect.bottom) {
dy = y - rect.bottom;
}

if (e.changedTouches && e.changedTouches.length > 0) {
return e.changedTouches[0];
}
return dx * dx + dy * dy;
}

export function getPositionHost(e) {
if (e.targetTouches && e.targetTouches.length > 0) {
return e.targetTouches[0];
}

return e;
if (e.changedTouches && e.changedTouches.length > 0) {
return e.changedTouches[0];
}

return e;
}
1 change: 1 addition & 0 deletions public/styles/app-header.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
background: var(--header-bg);
padding: 10px 20px;
position: relative;
user-select: none;
}

.app-header > .title {
Expand Down
5 changes: 0 additions & 5 deletions public/styles/todo-frame.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
.todo-frame {
position: relative;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

Expand Down
7 changes: 2 additions & 5 deletions public/styles/todo-item.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@
transform 0.2s ease-out,
opacity 0.2s ease-out;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
touch-action: none;
-webkit-touch-callout: none;
}

.todo-item > .checkbox {
Expand Down

0 comments on commit f4f2dea

Please sign in to comment.