swiss-army-knife-card icon indicating copy to clipboard operation
swiss-army-knife-card copied to clipboard

Explore new user actions like click, double click, hold, hold-repeat and drag on a single tool

Open AmoebeLabs opened this issue 1 year ago • 1 comments

The Problem To Be Solved

The current tools implement just two actions:

  • click (single) to perform an action like calling a service or script
  • drag to use one of the sliders (vertical/horizontal and circular)

Adding some other actions could extend the usage of some tools:

  • hold to call another popup screen for instance
  • hold-repeat to push a button that calls some service repeatedly while holding
  • hold-repeat-drag to accelerate repeat by dragging
  • double-click for popup or service calls

As these actions have relations, you must delay the single-click event as you wait for a double-click, only an integrated solution will work that can handle and distinguish all these events on a single tool!

Additional background:

Related Issues (if any)

(Optional): Suggested Solution

There is a dblClick event, but the internet (read: Google, Bing, CanIUse and ChatGPT) don't agree on the fact if this is supported by Android mobile devices...

Solution 1

While discussing this with ChatGPT, the following integrated basic event logic was created:

let clickTimeout;
let holdTimeout;
let repeatInterval;
let dragStartX;
let dragStartY;
let isDragging = false;

const element = document.getElementById('myElement');

// Handle click event
element.addEventListener('click', function(event) {
  if (clickTimeout !== undefined) {
    // A click has already been registered
    // This is the second click - handle as a double click
    clearTimeout(clickTimeout);
    clickTimeout = undefined;
    console.log('Double click detected!');
  } else {
    // Register the click and wait for another click
    clickTimeout = setTimeout(function() {
      clickTimeout = undefined;
      console.log('Single click detected!');
    }, 250);
  }
});

// Handle hold event
element.addEventListener('mousedown', function(event) {
  const startTime = Date.now();
  holdTimeout = setTimeout(function() {
    clearInterval(repeatInterval);
    repeatInterval = setInterval(function() {
      const timeElapsed = Date.now() - startTime;
      const dragDistanceX = Math.abs(dragStartX - event.clientX);
      const dragDistanceY = Math.abs(dragStartY - event.clientY);
      const dragDistance = Math.max(dragDistanceX, dragDistanceY);
      const speed = Math.max(1, Math.floor(dragDistance / 10));
      const interval = Math.max(200, Math.min(500, 1000 / speed));
      console.log('Hold and repeat with speed:', speed, 'and interval:', interval);
    }, Math.max(200, Math.min(500, 1000 / speed)));
    console.log('Hold detected!');
  }, 500);
});

element.addEventListener('mouseup', function(event) {
  clearInterval(repeatInterval);
  clearTimeout(holdTimeout);
  isDragging = false;
});

// Handle drag event
element.addEventListener('mousedown', function(event) {
  isDragging = true;
  dragStartX = event.clientX;
  dragStartY = event.clientY;
});

element.addEventListener('mousemove', function(event) {
  if (isDragging) {
    const dragEndX = event.clientX;
    const dragEndY = event.clientY;
    const dragDistanceX = dragEndX - dragStartX;
    const dragDistanceY = dragEndY - dragStartY;
    console.log('Dragged by: (' + dragDistanceX + ', ' + dragDistanceY + ')');
    // Perform some action in response to the drag movement
  }
});

Of course, this does NOT take into account all the browser dependencies and how Lit works. Click and Touch (mobile) event propagation is not blocked as you can see in the above examples. But at least this can be a start for experimenting.

Furthermore, ChatGPT gives me two event handlers for 'mousedown', which obviously should be integrated I think.

I can check both Chrome/Edge on Win11 and Mobile Safari on my iPhone and iPad. I can't check Safari and Android (which is basically Chrome with some exceptions), but in general, it will be fine.

The ChatGPT questions I asked were as follows (ChatGPT not always understand me right...)

  • show me the javascript solution to catch hold and double click in a browser
  • Nice! And a javascript example to implement "hold"? (Note: ChatGPT only did the double click)
  • And an handler that catches both "click" and "double click"?
  • And if I want the "hold" to be extended with "hold and repeat"?
  • Is the dblclick event supported by Androids WebView? (From now on, ChatGPT does not use the dblClick event anymore in the proposed solutions!)
  • I forgot the "drag" action. So I have "click", "double click", "hold", "hold and repeat", and "drag"
  • Nice! And if I want to do something while dragging? Should I use mousemove events orso?
  • I'd like to use "drag distance" to accelerate/deaccelerate the "hold-repeat" action. Can that be integrated in the existing logic?
  • Can I use the speed for the hold-repeat timer? Say that the timer varies with speed between 200 and 500 msec? (ChatGPT did not understand that I want to use that calculated speed/drag distance for the repeat timer)

Solution 2

Solution 1 gave me two mousedown eventhandlers, so I asked ChatGPT to:

  • I see two mousedown eventhandlers. Can that be done in a single eventhandler?

This time, ChatGPT gives me a completely different implementation, where the hold-repeat-drag is time-limited. Also, hold will cancel drag, which is not what I want. If I push a button and hold it for repeat, I still want to be able to drag the mouse/finger to influence the repeat timer!

In this implementation, we create a single mousedown event listener that registers both the hold-and-repeat and drag actions. When the mouse is pressed, we start a timer to check if the mouse is being held down for a long time, and we also register a mousemove listener to detect drag movements.

If the mouse is held down for a long time, we start the hold-and-repeat action and also remove the mousemove listener, indicating that the hold-and-repeat action is in progress and the drag action is no longer active. We also set a timer to check if the mouse is held down for too long (in case the user intended to perform a short hold instead of a long hold), and remove the mousemove listener when the time is up.

If the mouse is not held down for a long time, we assume that the user intends to perform a drag

let clickTimeout;
let holdTimeout;
let repeatInterval;
let dragStartX;
let dragStartY;
let isDragging = false;

const element = document.getElementById('myElement');

// Handle click event
element.addEventListener('click', function(event) {
  if (clickTimeout !== undefined) {
    // A click has already been registered
    // This is the second click - handle as a double click
    clearTimeout(clickTimeout);
    clickTimeout = undefined;
    console.log('Double click detected!');
  } else {
    // Register the click and wait for another click
    clickTimeout = setTimeout(function() {
      clickTimeout = undefined;
      console.log('Single click detected!');
    }, 250);
  }
});

// Handle hold and repeat events
element.addEventListener('mousedown', function(event) {
  const startTime = Date.now();
  let interval = 500;
  let speed = 1;
  holdTimeout = setTimeout(function() {
    clearInterval(repeatInterval);
    repeatInterval = setInterval(function() {
      const timeElapsed = Date.now() - startTime;
      const dragDistanceX = Math.abs(dragStartX - event.clientX);
      const dragDistanceY = Math.abs(dragStartY - event.clientY);
      const dragDistance = Math.max(dragDistanceX, dragDistanceY);
      speed = Math.max(1, Math.floor(dragDistance / 10));
      interval = Math.max(200, Math.min(500, 1000 / speed));
      console.log('Hold and repeat with speed:', speed, 'and interval:', interval);
    }, interval);
    console.log('Hold detected!');
  }, 500);

  const checkHold = setInterval(() => {
    if ((Date.now() - startTime) >= 500) {
      clearInterval(checkHold);
      element.removeEventListener('mousemove', handleDrag);
      isDragging = false;
    }
  }, 100);

  const handleDrag = (event) => {
    if (isDragging) {
      const dragEndX = event.clientX;
      const dragEndY = event.clientY;
      const dragDistanceX = dragEndX - dragStartX;
      const dragDistanceY = dragEndY - dragStartY;
      console.log('Dragged by: (' + dragDistanceX + ', ' + dragDistanceY + ')');
      // Perform some action in response to the drag movement
    }
  };

  element.addEventListener('mousemove', handleDrag);
});

element.addEventListener('mouseup', function(event) {
  clearInterval(repeatInterval);
  clearTimeout(holdTimeout);
  isDragging = false;
  element.removeEventListener('mousemove', handleDrag);
});

Solution 3

As Solution 2 is a nice variation, but not always what I want, I asked ChatGPT to change this.

  • In this solution hold cancels drag for hold-and-repeat. But I want both. The previous solution did that, but used two mousedown eventhandlers. Can I have the previous functionality, but then with a single mousedown eventhandler?

Yes, you can modify the code to allow both the hold-and-repeat and drag actions to be performed simultaneously while using a single mousedown event handler. Here's an updated implementation that does that:

In this implementation, we create a single mousedown event listener that registers both the hold-and-repeat and drag actions. When the mouse is pressed, we start a timer to check if the mouse is being held down for a long time, and we also register a mousemove listener to detect drag movements.

If the mouse is held down for a long time, we start the hold-and-repeat action, but we do not remove the mousemove listener, indicating that both the hold-and-repeat and drag actions can be performed simultaneously.

If the mouse is not held down for a long time, we assume that the user intends to perform a drag. We record the start position of the drag, set a flag to indicate that a drag is in progress, and register the mousemove listener to detect drag movements.

When the mouse is released, we stop the hold-and-repeat action, clear the hold and repeat timers, and remove the mousemove listener.

let clickTimeout;
let holdTimeout;
let repeatInterval;
let dragStartX;
let dragStartY;
let isDragging = false;

const element = document.getElementById('myElement');

// Handle click event
element.addEventListener('click', function(event) {
  if (clickTimeout !== undefined) {
    // A click has already been registered
    // This is the second click - handle as a double click
    clearTimeout(clickTimeout);
    clickTimeout = undefined;
    console.log('Double click detected!');
  } else {
    // Register the click and wait for another click
    clickTimeout = setTimeout(function() {
      clickTimeout = undefined;
      console.log('Single click detected!');
    }, 250);
  }
});

// Handle hold and repeat events
element.addEventListener('mousedown', function(event) {
  const startTime = Date.now();
  let interval = 500;
  let speed = 1;
  holdTimeout = setTimeout(function() {
    clearInterval(repeatInterval);
    repeatInterval = setInterval(function() {
      const timeElapsed = Date.now() - startTime;
      const dragDistanceX = Math.abs(dragStartX - event.clientX);
      const dragDistanceY = Math.abs(dragStartY - event.clientY);
      const dragDistance = Math.max(dragDistanceX, dragDistanceY);
      speed = Math.max(1, Math.floor(dragDistance / 10));
      interval = Math.max(200, Math.min(500, 1000 / speed));
      console.log('Hold and repeat with speed:', speed, 'and interval:', interval);
    }, interval);
    console.log('Hold detected!');
  }, 500);

  const handleDrag = (event) => {
    if (isDragging) {
      const dragEndX = event.clientX;
      const dragEndY = event.clientY;
      const dragDistanceX = dragEndX - dragStartX;
      const dragDistanceY = dragEndY - dragStartY;
      console.log('Dragged by: (' + dragDistanceX + ', ' + dragDistanceY + ')');
      // Perform some action in response to the drag movement
    }
  };

  if (!isDragging) {
    dragStartX = event.clientX;
    dragStartY = event.clientY;
    isDragging = true;
    console.log('Drag started!');
  }

  element.addEventListener('mousemove', handleDrag);
});

element.addEventListener('mouseup', function(event) {
  clearInterval(repeatInterval);
  clearTimeout(holdTimeout);
  isDragging = false;
  element.removeEventListener('mousemove', handleDrag);
});

(Optional): Alternative Solutions

The current click and drag handling is already custom-made for this card as libraries just didn't work with mobile Safari, ie Touch events, so the solution above seems to go in this direction too!

For instance, the following explanation is exactly how the current drag for sliders is implemented by me already:

Note that we set a boolean isDragging variable to true when the mousedown event is fired, and set it back to false when the mouseup event is fired. This way, we can track whether the mouse is currently dragging or not, and only perform the action in response to mousemove events when dragging is in progress.

AmoebeLabs avatar May 12 '23 12:05 AmoebeLabs