diff --git a/.stylelintrc.json b/.stylelintrc.json index 5f645f5..c7e48af 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -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]+)*$" } } diff --git a/README.md b/README.md index b3adb12..8dad12d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/public/index.html b/public/index.html index 8fa536b..c0059c5 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,10 @@ - + VANILLA TODO diff --git a/public/scripts/AppDraggable.js b/public/scripts/AppDraggable.js index 0776973..91a6134 100644 --- a/public/scripts/AppDraggable.js +++ b/public/scripts/AppDraggable.js @@ -4,18 +4,21 @@ * 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; @@ -23,13 +26,13 @@ export function AppDraggable(el, options) { 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(); } @@ -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; @@ -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; @@ -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; @@ -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() { @@ -144,8 +137,6 @@ export function AppDraggable(el, options) { } function dispatchTarget() { - if (!dragging) return; - const nextTarget = getTarget(); if (nextTarget === currentTarget) return; @@ -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', { @@ -190,7 +176,7 @@ export function AppDraggable(el, options) { ); } - setTimeout(dispatchOver, 50); + setTimeout(dispatchOverContinuously, 50); } 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() { @@ -312,12 +327,12 @@ 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? @@ -325,30 +340,40 @@ export function AppDraggable(el, options) { } // 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; - - return dx * dx + dy * dy; - } - - function getPositionHost(e) { - if (e.targetTouches && e.targetTouches.length > 0) { - return e.targetTouches[0]; - } - - if (e.changedTouches && e.changedTouches.length > 0) { - return e.changedTouches[0]; - } - - return e; - } +} + +export function pointDistanceToRectSquared(x, y, rect) { + let dx = 0; + let dy = 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; +} + +export function getPositionHost(e) { + if (e.targetTouches && e.targetTouches.length > 0) { + return e.targetTouches[0]; + } + + if (e.changedTouches && e.changedTouches.length > 0) { + return e.changedTouches[0]; + } + + return e; } diff --git a/public/styles/app-header.css b/public/styles/app-header.css index 6a14110..a7443f7 100644 --- a/public/styles/app-header.css +++ b/public/styles/app-header.css @@ -2,6 +2,7 @@ background: var(--header-bg); padding: 10px 20px; position: relative; + user-select: none; } .app-header > .title { diff --git a/public/styles/todo-frame.css b/public/styles/todo-frame.css index 095c393..3f25c38 100644 --- a/public/styles/todo-frame.css +++ b/public/styles/todo-frame.css @@ -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; } diff --git a/public/styles/todo-item.css b/public/styles/todo-item.css index c436318..d89405f 100644 --- a/public/styles/todo-item.css +++ b/public/styles/todo-item.css @@ -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 {