Drag and Drop widget behaving as if it had ClickLock

0
Hi, Is anyone else experiencing an issue with the Drag and Drop widget (https://marketplace.mendix.com/link/component/116604) where clicking and holding on the draggable object doesn't start the drag action? I'm finding that I have to click and release the mouse button to start the drag action, and then click and release again on the drop area to drop the item. It's behaving as if ClickLock is enabled, a feature of Windows machines which I wasn't aware of until now and that I don't thing affects how web apps behave anyway, but it seems to describe exactly what's happening.   This issue started a few weeks ago and I've verified it occurs on Chrome, Edge, and Firefox. Interestingly, when toggling the device toolbar from Chrome's dev tools, the issue is resolved and the dragging action behaves normally with a click+hold-drag-release action. The viewport can even be the same dimensions as when the device toolbar is not toggled, and the issue is still resolved. We haven't updated the widget, and it hasn't had any new releases since 2022. My clients are also experiencing this behavior, and it's significantly impacting usability. For reference, I'm using Mendix Studio Pro version 10.6.7.   To troubleshoot, I've branched out an older release of the app from when this was working fine, and the issue persists there as well.   Any feedback would be much appreciated. Thanks, Raul
asked
3 answers
2

I faced a similar issue recently and noticed that while inspecting the browser with the device tool, the browser uses touch events instead of mouse events, and it was working right. To address this, I created a JavaScript snippet to convert mouse events into touch events, and it worked flawlessly. You can use the following JavaScript code to make it work:

 

function triggerTouchEvent(eventType, originalEvent) {
    console.log(`Triggering ${eventType} event for:`, originalEvent.target);
    
    // Create the Touch object
    const touchObj = new Touch({
        identifier: Date.now(),
        target: originalEvent.target,
        clientX: originalEvent.clientX,
        clientY: originalEvent.clientY,
        radiusX: 2.5,
        radiusY: 2.5,
        rotationAngle: 0,
        force: 1.0,
    });
    console.log('Touch object created:', touchObj);
    
    // Create and dispatch the TouchEvent
    const touchEvent = new TouchEvent(eventType, {
        cancelable: true,
        bubbles: true,
        touches: [touchObj],
        targetTouches: [],
        changedTouches: [touchObj],
        shiftKey: true,
    });
    console.log('Dispatching touch event:', touchEvent);
    originalEvent.target.dispatchEvent(touchEvent);
    console.log(`${eventType} event dispatched.`);
}

document.addEventListener('mousedown', function(e) {
    console.log('Mouse down event detected.');
    e.preventDefault(); // Prevent the default click behavior
    triggerTouchEvent('touchstart', e);
}, { passive: false });

document.addEventListener('mousemove', function(e) {
    console.log('Mouse move event detected.');
    e.preventDefault(); // Prevent the default click behavior
    triggerTouchEvent('touchmove', e);
}, { passive: false });

document.addEventListener('mouseup', function(e) {
    console.log('Mouse up event detected.');
    e.preventDefault(); // Prevent the default click behavior
    triggerTouchEvent('touchend', e);
}, { passive: false });

 

answered
1

Hi Eduardo, your solution worked well for me. 

I found a way to address Raul's question about clicking inputs. You can add a condition that checks if the event target is an input, textarea, or otherwise form-related element. If the event is triggered on an input element, the code will not generate the custom touch event and will allow the default desktop behavior.

Hopefully Mendix will soon update the widget to make this an option for both device types (touch events only). 

 

Just use this updated code:

 

function triggerTouchEvent(eventType, originalEvent) {
    console.log(`Triggering ${eventType} event for:`, originalEvent.target);

    // Create the Touch object
    const touchObj = new Touch({
        identifier: Date.now(),
        target: originalEvent.target,
        clientX: originalEvent.clientX,
        clientY: originalEvent.clientY,
        radiusX: 2.5,
        radiusY: 2.5,
        rotationAngle: 0,
        force: 1.0,
    });
    console.log('Touch object created:', touchObj);

    // Create and dispatch the TouchEvent
    const touchEvent = new TouchEvent(eventType, {
        cancelable: true,
        bubbles: true,
        touches: [touchObj],
        targetTouches: [],
        changedTouches: [touchObj],
        shiftKey: true,
    });
    console.log('Dispatching touch event:', touchEvent);
    originalEvent.target.dispatchEvent(touchEvent);
    console.log(`${eventType} event dispatched.`);
}

function shouldSkipTouchEvent(target) {
    // Skip inputs, textareas, select elements, buttons, and links
    return target.tagName === 'INPUT' || 
           target.tagName === 'TEXTAREA' || 
           target.tagName === 'SELECT' || 
           target.tagName === 'BUTTON' || 
           target.tagName === 'A';
}

// Handle clicking outside inputs to blur (deselect) input fields
document.addEventListener('mousedown', function(e) {
    console.log('Mouse down event detected.');

    // If the click target is not an input or clickable element, blur the active element
    if (!shouldSkipTouchEvent(e.target) && document.activeElement && 
        (document.activeElement.tagName === 'INPUT' || 
         document.activeElement.tagName === 'TEXTAREA' || 
         document.activeElement.tagName === 'SELECT')) {
        document.activeElement.blur();  // Remove focus from the active input
        console.log('Active input blurred.');
    }

    if (shouldSkipTouchEvent(e.target)) {
        console.log('Mouse down on input/button/link, skipping touch event.');
        return; // Do not trigger touch events on input fields, buttons, or links
    }

    e.preventDefault(); // Prevent the default click behavior
    triggerTouchEvent('touchstart', e);
}, { passive: false });

document.addEventListener('mousemove', function(e) {
    console.log('Mouse move event detected.');

    if (shouldSkipTouchEvent(e.target)) {
        console.log('Mouse move on input/button/link, skipping touch event.');
        return; // Do not trigger touch events on input fields, buttons, or links
    }

    e.preventDefault(); // Prevent the default click behavior
    triggerTouchEvent('touchmove', e);
}, { passive: false });

document.addEventListener('mouseup', function(e) {
    console.log('Mouse up event detected.');

    if (shouldSkipTouchEvent(e.target)) {
        console.log('Mouse up on input/button/link, skipping touch event.');
        return; // Do not trigger touch events on input fields, buttons, or links
    }

    e.preventDefault(); // Prevent the default click behavior
    triggerTouchEvent('touchend', e);
}, { passive: false });

 

answered
1

Hi all,

 

Thanks Eduardo and Gina for your solutions. 

 

I have modified the snippet so it will only create touch events for drag and drop items. As all draggable containers have a parent class with .dnd_draggable_item. This removes the need for specific exceptions for inputs.

 

EDIT: Instead of always triggering for mouse movements, now only do so if a draggable item is clicked

 

function triggerTouchEvent(eventType, originalEvent) {
    // Create the Touch object
    const touchObj = new Touch({
        identifier: Date.now(),
        target: originalEvent.target,
        clientX: originalEvent.clientX,
        clientY: originalEvent.clientY,
        radiusX: 2.5,
        radiusY: 2.5,
        rotationAngle: 0,
        force: 1.0,
    });

    // Create and dispatch the TouchEvent
    const touchEvent = new TouchEvent(eventType, {
        cancelable: true,
        bubbles: true,
        touches: [touchObj],
        targetTouches: [],
        changedTouches: [touchObj],
        shiftKey: true,
    });
    originalEvent.target.dispatchEvent(touchEvent);
}

function hasDraggableItemClass(target) {
    // Check if the target or any of its parents have the .dnd_draggable_item class
    return target.closest('.dnd_draggable_item') !== null;
}

// Variable to track the dragging state
let isDragging = false;

function onMouseDown(e) {
    if (!hasDraggableItemClass(e.target)) {
        return; // Only process events on .dnd_draggable_item elements
    }

    e.preventDefault(); // Prevent the default mouse behavior
    isDragging = true;

    triggerTouchEvent('touchstart', e);

    // Add event listeners for mousemove and mouseup
    document.addEventListener('mousemove', onMouseMove, { passive: false });
    document.addEventListener('mouseup', onMouseUp, { passive: false });
}

function onMouseMove(e) {
    if (!isDragging) {
        return;
    }

    e.preventDefault();
    triggerTouchEvent('touchmove', e);
}

function onMouseUp(e) {
    if (!isDragging) {
        return;
    }

    e.preventDefault();
    triggerTouchEvent('touchend', e);

    isDragging = false;

    // Remove the event listeners since dragging has ended
    document.removeEventListener('mousemove', onMouseMove, { passive: false });
    document.removeEventListener('mouseup', onMouseUp, { passive: false });
}

// Attach the mousedown event listener
document.addEventListener('mousedown', onMouseDown, { passive: false });

 

answered