1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-21 13:21:29 +02:00

improve drag and drop w/ auto scroll at border

This commit is contained in:
Morris Brodersen
2024-12-31 13:08:50 +01:00
parent 93e0766bea
commit f4f2dea25a
7 changed files with 89 additions and 67 deletions

View File

@@ -1,7 +1,6 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"rules": { "rules": {
"property-no-vendor-prefix": null,
"selector-class-pattern": "^[\\-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$" "selector-class-pattern": "^[\\-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$"
} }
} }

View File

@@ -891,6 +891,8 @@ Thanks!
- Add dark mode - Add dark mode
- Add Playwright config for testing more browsers - 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 - Update dependencies
### 08/2024 ### 08/2024

View File

@@ -3,7 +3,10 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" /> <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" /> <meta name="theme-color" content="#fefefe" />
<title>VANILLA TODO</title> <title>VANILLA TODO</title>

View File

@@ -4,18 +4,21 @@
* dropSelector: string; * dropSelector: string;
* dragThreshold?: number; * dragThreshold?: number;
* dropRange?: number; * dropRange?: number;
* scrollThreshold?: number;
* scrollSpeed?: number;
* }} options * }} options
*/ */
export function AppDraggable(el, options) { export function AppDraggable(el, options) {
const dragThreshold = options.dragThreshold ?? 5; const dragThreshold = options.dragThreshold ?? 5;
const dropRange = options.dropRange ?? 50; const dropRange = options.dropRange ?? 50;
const dropRangeSquared = dropRange * dropRange; const dropRangeSquared = dropRange * dropRange;
const scrollThreshold = options.scrollThreshold ?? 12;
const scrollSpeed = options.scrollSpeed ?? 7;
let originX, originY; let originX, originY;
let clientX, clientY; let clientX, clientY;
let startTime; let startTime;
let dragging = false; let dragging = false;
let clicked = false;
let data; let data;
let image; let image;
let imageSource; let imageSource;
@@ -23,13 +26,13 @@ export function AppDraggable(el, options) {
let currentTarget; let currentTarget;
el.addEventListener('touchstart', start, { passive: true }); 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( el.addEventListener(
'click', 'click',
(e) => { (e) => {
if (dragging || clicked) { if (dragging) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
} }
@@ -42,8 +45,6 @@ export function AppDraggable(el, options) {
if (e.type === 'mousedown' && e.button !== 0) return; if (e.type === 'mousedown' && e.button !== 0) return;
if (e.touches && e.touches.length > 1) return; if (e.touches && e.touches.length > 1) return;
e.preventDefault();
const p = getPositionHost(e); const p = getPositionHost(e);
clientX = originX = p.clientX ?? p.pageX; clientX = originX = p.clientX ?? p.pageX;
clientY = originY = p.clientY ?? p.pageY; clientY = originY = p.clientY ?? p.pageY;
@@ -53,8 +54,6 @@ export function AppDraggable(el, options) {
} }
function move(e) { function move(e) {
e.preventDefault();
const p = getPositionHost(e); const p = getPositionHost(e);
clientX = p.clientX ?? p.pageX; clientX = p.clientX ?? p.pageX;
clientY = p.clientY ?? p.pageY; clientY = p.clientY ?? p.pageY;
@@ -72,7 +71,7 @@ export function AppDraggable(el, options) {
return; return;
} }
// prevent unintentional dragging on touch devices // Prevent unintentional dragging on touch devices
if (e.touches && Date.now() - startTime < 50) { if (e.touches && Date.now() - startTime < 50) {
stopListening(); stopListening();
return; return;
@@ -85,33 +84,27 @@ export function AppDraggable(el, options) {
dispatchDrag(); dispatchDrag();
dispatchTarget(); dispatchTarget();
dispatchOverContinuously(); dispatchOverContinuously();
autoScroll();
} }
function end(e) { function end() {
e.preventDefault();
if (!dragging) {
e.target.click();
clicked = true;
}
stopListening(); stopListening();
requestAnimationFrame(() => { requestAnimationFrame(() => {
clicked = false;
if (dragging) { if (dragging) {
dispatchTarget(); dispatchTarget();
dispatchEnd(); dispatchEnd();
dragging = false;
} }
}); });
} }
function startListening() { function startListening() {
el.addEventListener('touchmove', move); el.addEventListener('touchmove', move, { passive: true });
el.addEventListener('touchend', end); el.addEventListener('touchend', end, { passive: true });
window.addEventListener('mousemove', move); window.addEventListener('mousemove', move, { passive: true });
window.addEventListener('mouseup', end); window.addEventListener('mouseup', end, { passive: true });
} }
function stopListening() { function stopListening() {
@@ -144,8 +137,6 @@ export function AppDraggable(el, options) {
} }
function dispatchTarget() { function dispatchTarget() {
if (!dragging) return;
const nextTarget = getTarget(); const nextTarget = getTarget();
if (nextTarget === currentTarget) return; if (nextTarget === currentTarget) return;
@@ -176,11 +167,6 @@ export function AppDraggable(el, options) {
function dispatchOverContinuously() { function dispatchOverContinuously() {
if (!dragging) return; if (!dragging) return;
dispatchOver();
setTimeout(dispatchOver, 50);
}
function dispatchOver() {
if (currentTarget) { if (currentTarget) {
currentTarget.dispatchEvent( currentTarget.dispatchEvent(
new CustomEvent('draggableOver', { new CustomEvent('draggableOver', {
@@ -190,7 +176,7 @@ export function AppDraggable(el, options) {
); );
} }
setTimeout(dispatchOver, 50); setTimeout(dispatchOverContinuously, 50);
} }
function dispatchEnd() { function dispatchEnd() {
@@ -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() { function buildDetail() {
@@ -312,12 +327,12 @@ export function AppDraggable(el, options) {
candidates.push({ candidates.push({
el, el,
distance2: distanceSquared, distanceSquared,
}); });
}); });
candidates.sort((a, b) => { 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 // in this case, the client position is inside both rectangles
// if A contains B, B is the correct target and vice versa // if A contains B, B is the correct target and vice versa
// TODO sort by z-index somehow? // TODO sort by z-index somehow?
@@ -325,22 +340,33 @@ export function AppDraggable(el, options) {
} }
// sort by distance, ascending // sort by distance, ascending
return a.distance2 - b.distance2; return a.distanceSquared - b.distanceSquared;
}); });
return candidates.length > 0 ? candidates[0].el : null; return candidates.length > 0 ? candidates[0].el : null;
} }
}
function pointDistanceToRectSquared(x, y, rect) { export function pointDistanceToRectSquared(x, y, rect) {
const dx = let dx = 0;
x < rect.left ? x - rect.left : x > rect.right ? x - rect.right : 0; let dy = 0;
const dy =
y < rect.top ? y - rect.top : y > rect.bottom ? y - rect.bottom : 0; if (x < rect.left) {
dx = x - rect.left;
} else if (x > rect.right) {
dx = x - rect.right;
}
if (y < rect.top) {
dy = y - rect.top;
} else if (y > rect.bottom) {
dy = y - rect.bottom;
}
return dx * dx + dy * dy; return dx * dx + dy * dy;
} }
function getPositionHost(e) { export function getPositionHost(e) {
if (e.targetTouches && e.targetTouches.length > 0) { if (e.targetTouches && e.targetTouches.length > 0) {
return e.targetTouches[0]; return e.targetTouches[0];
} }
@@ -351,4 +377,3 @@ export function AppDraggable(el, options) {
return e; return e;
} }
}

View File

@@ -2,6 +2,7 @@
background: var(--header-bg); background: var(--header-bg);
padding: 10px 20px; padding: 10px 20px;
position: relative; position: relative;
user-select: none;
} }
.app-header > .title { .app-header > .title {

View File

@@ -1,10 +1,5 @@
.todo-frame { .todo-frame {
position: relative; 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; user-select: none;
} }

View File

@@ -9,12 +9,9 @@
transform 0.2s ease-out, transform 0.2s ease-out,
opacity 0.2s ease-out; opacity 0.2s ease-out;
cursor: pointer; 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; user-select: none;
touch-action: none;
-webkit-touch-callout: none;
} }
.todo-item > .checkbox { .todo-item > .checkbox {