AmoebeLabs / swiss-army-knife-card

The versatile custom Swiss Army Knife card for Home Assistant allows you to create your unique visualization using several graphical tools, styling options and animations.
223 stars 19 forks source link

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

Open AmoebeLabs opened 1 year ago

AmoebeLabs commented 1 year ago

The Problem To Be Solved

The current tools implement just two actions:

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

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...)

Solution 2

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

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.

```javascript
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.

tuupos commented 8 months ago

I know you have a lot of work already done but is there any progress tot be expected from this? I would really help extending mu button functionality (e.g. browser_mod popup with a long press). Can I alternatively also use the drag action as a work around so I could slide over a a button (not a slider) to get a different action than tap_action e.g. on your comment in the helper files: "fill: rgba(0,0,0,0) for 'invisible' backgrounds for circles/rectangles that need interaction!" Can i make a "drag"movement over the rectangle to initiate a different action than the tap action?