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:
@@ -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]+)*$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
|
||||||
|
|
||||||
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 (y < rect.top) {
|
||||||
|
dy = y - rect.top;
|
||||||
|
} else if (y > rect.bottom) {
|
||||||
|
dy = y - rect.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
}
|
}
|
||||||
@@ -350,5 +376,4 @@ export function AppDraggable(el, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user