From 7f1038bef37bd8edc27ff6744ecc54ca2034a2ff Mon Sep 17 00:00:00 2001 From: Morris Brodersen Date: Wed, 21 Oct 2020 20:18:12 +0200 Subject: [PATCH] mobile improvements --- README.md | 4 + public/scripts/AppDraggable.js | 172 ++++++++++++++++++-------------- public/scripts/TodoItemInput.js | 16 +-- public/styles/todo-frame.css | 5 + public/styles/todo-item.css | 6 ++ 5 files changed, 122 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 2235f12..ecd86e7 100644 --- a/README.md +++ b/README.md @@ -738,3 +738,7 @@ Projects I've inspected for drag & drop architecture: - [React DnD](https://react-dnd.github.io) - [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) - [dragula](https://github.com/bevacqua/dragula) + +Other interesting articles: + +- [The case for vanilla front-end development (pushdata.io)](https://pushdata.io/blog/1) diff --git a/public/scripts/AppDraggable.js b/public/scripts/AppDraggable.js index 367082d..2a537b5 100644 --- a/public/scripts/AppDraggable.js +++ b/public/scripts/AppDraggable.js @@ -8,6 +8,7 @@ VT.AppDraggable = function (el, options) { var originX, originY; var clientX, clientY; + var startTime; var dragging = false; var clicked = false; var data; @@ -40,13 +41,75 @@ VT.AppDraggable = function (el, options) { function start(e) { if (el.classList.contains('_nodrag')) return; if (e.type === 'mousedown' && e.button !== 0) return; + if (e.touches && e.touches.length > 1) return; e.preventDefault(); var p = getPositionHost(e); clientX = originX = p.clientX || p.pageX; clientY = originY = p.clientY || p.pageY; + startTime = Date.now(); + startListening(); + } + + function move(e) { + e.preventDefault(); + + var p = getPositionHost(e); + clientX = p.clientX || p.pageX; + clientY = p.clientY || p.pageY; + + if (dragging) { + dispatchDrag(); + dispatchTarget(); + return; + } + + var deltaX = clientX - originX; + var deltaY = clientY - originY; + + if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) { + return; + } + + // prevent unintentional dragging on touch devices + if (e.touches && Date.now() - startTime < 50) { + stopListening(); + return; + } + + dragging = true; + data = {}; + + dispatchStart(); + dispatchDrag(); + dispatchTarget(); + dispatchOverContinuously(); + } + + function end(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + if (!dragging) { + e.target.click(); + clicked = true; + } + + stopListening(); + + requestAnimationFrame(function () { + clicked = false; + + if (dragging) { + dispatchTarget(); + dispatchEnd(); + } + }); + } + + function startListening() { if (window.navigator.pointerEnabled) { el.addEventListener('pointermove', move); el.addEventListener('pointerup', end); @@ -61,61 +124,24 @@ VT.AppDraggable = function (el, options) { } } - function move(e) { - e.preventDefault(); - - var p = getPositionHost(e); - clientX = p.clientX || p.pageX; - clientY = p.clientY || p.pageY; - - if (dragging) return; - - var deltaX = clientX - originX; - var deltaY = clientY - originY; - - if (Math.abs(deltaX) < dragThreshold && Math.abs(deltaY) < dragThreshold) { - return; + function stopListening() { + if (window.navigator.pointerEnabled) { + el.removeEventListener('pointermove', move); + el.removeEventListener('pointerup', end); + } else if (window.navigator.msPointerEnabled) { + el.removeEventListener('MSPointerMove', move); + el.removeEventListener('MSPointerUp', end); + } else { + window.removeEventListener('mousemove', move); + window.removeEventListener('mouseup', end); + el.removeEventListener('touchmove', move); + el.removeEventListener('touchend', end); } - - dispatchStart(); - dispatchLoop(); - dispatchOver(); - } - - function end(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - - if (!dragging) { - e.target.click(); - clicked = true; - } - - requestAnimationFrame(function () { - dragging = false; - clicked = false; - - if (window.navigator.pointerEnabled) { - el.removeEventListener('pointermove', move); - el.removeEventListener('pointerup', end); - } else if (window.navigator.msPointerEnabled) { - el.removeEventListener('MSPointerMove', move); - el.removeEventListener('MSPointerUp', end); - } else { - window.removeEventListener('mousemove', move); - window.removeEventListener('mouseup', end); - el.removeEventListener('touchmove', move); - el.removeEventListener('touchend', end); - } - }); } // function dispatchStart() { - dragging = true; - data = {}; - setImage(el); el.dispatchEvent( @@ -126,17 +152,6 @@ VT.AppDraggable = function (el, options) { ); } - function dispatchLoop() { - dispatchDrag(); - dispatchTarget(); - - if (dragging) { - requestAnimationFrame(dispatchLoop); - } else { - dispatchEnd(); - } - } - function dispatchDrag() { image.dispatchEvent( new CustomEvent('draggableDrag', { @@ -147,6 +162,8 @@ VT.AppDraggable = function (el, options) { } function dispatchTarget() { + if (!dragging) return; + var nextTarget = getTarget(); if (nextTarget === currentTarget) return; @@ -174,6 +191,26 @@ VT.AppDraggable = function (el, options) { currentTarget = nextTarget; } + function dispatchOverContinuously() { + if (!dragging) return; + + dispatchOver(); + setTimeout(dispatchOver, 50); + } + + function dispatchOver() { + if (currentTarget) { + currentTarget.dispatchEvent( + new CustomEvent('draggableOver', { + detail: buildDetail(), + bubbles: true, + }) + ); + } + + setTimeout(dispatchOver, 50); + } + function dispatchEnd() { if (currentTarget) { currentTarget.addEventListener('draggableDrop', cleanUpOnce); @@ -193,21 +230,6 @@ VT.AppDraggable = function (el, options) { } } - function dispatchOver() { - if (!dragging) return; - - if (currentTarget) { - currentTarget.dispatchEvent( - new CustomEvent('draggableOver', { - detail: buildDetail(), - bubbles: true, - }) - ); - } - - setTimeout(dispatchOver, 50); - } - // function buildDetail() { diff --git a/public/scripts/TodoItemInput.js b/public/scripts/TodoItemInput.js index f2edf49..294bf58 100644 --- a/public/scripts/TodoItemInput.js +++ b/public/scripts/TodoItemInput.js @@ -10,12 +10,10 @@ VT.TodoItemInput = function (el) { var inputEl = el.querySelector('.input'); var saveEl = el.querySelector('.save'); + VT.AppLateBlur(inputEl); el.querySelectorAll('.app-icon').forEach(VT.AppIcon); - saveEl.addEventListener('click', save); - inputEl.addEventListener('keypress', handleKeypress); - - function handleKeypress(e) { + inputEl.addEventListener('keypress', function (e) { switch (e.keyCode) { case 13: // enter save(); @@ -24,7 +22,14 @@ VT.TodoItemInput = function (el) { clear(); break; } - } + }); + + inputEl.addEventListener('lateBlur', save); + + saveEl.addEventListener('click', function () { + save(); + inputEl.focus(); + }); function save() { var label = inputEl.value.trim(); @@ -39,7 +44,6 @@ VT.TodoItemInput = function (el) { ); inputEl.value = ''; - inputEl.focus(); } function clear() { diff --git a/public/styles/todo-frame.css b/public/styles/todo-frame.css index e5c525b..29d2111 100644 --- a/public/styles/todo-frame.css +++ b/public/styles/todo-frame.css @@ -1,6 +1,11 @@ .todo-frame { position: relative; overflow: hidden; + -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 f6249d7..42be60e 100644 --- a/public/styles/todo-item.css +++ b/public/styles/todo-item.css @@ -7,6 +7,12 @@ background: #fff; transition: 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; } .todo-item > .checkbox {