bevacqua / dragula

:ok_hand: Drag and drop so simple it hurts
https://bevacqua.github.io/dragula/
MIT License
22.06k stars 1.97k forks source link

Lift event / start dragging after touch delay #289

Open pleaseshutup opened 8 years ago

pleaseshutup commented 8 years ago

In order to make this library a bit more touch friendly it will help to be able to delay dragging until after a second of holding the finger down without moving. This way a user can drag if they want or also scroll an area on a touch device.

I see there was a commit with a new method called "lift" which will work perfectly for me. When will this be available in dist folder?

fabsor commented 8 years ago

I'm also interested in this feature. Our use case is the same; we have an area where you can scroll, and it would be nice a delay. There is an old closed PR, #225

Are there any changes that would have to be done to the PR to be a viable solution?

ramiabraham commented 8 years ago

Same UC here, fwiw. Hacked in a setTimeout workaround for now, but I admittedly don't feel great about that solution :)

Edit: see better option below.

reid-rigo commented 8 years ago

I'm in the same boat. Trying to make scrolling on mobile not kick off drags. @ramiabraham how does your workaround work?

ramiabraham commented 8 years ago

@rigoleto

Hope that helps!

avand commented 8 years ago

@rigoleto this is a great workaround. In my case, I'm building a todo list, and pretty much the whole UI is draggable. Does this mean putting a mask over the whole viewport? And if I do that, won't I affect normal link clicks? And did you adapt the jQuery scroll intention plugin?

linearza commented 8 years ago

I would also prefer if we had control over triggering drags with different events. We use hammerjs for example and so would prefer to activate drags on press rather. The lift api could be the solution

regislutter commented 8 years ago

I also needed this feature in my project to manage scroll and drag'n drop on iPad.

For anyone interested, I managed to make it work and referred to this commit: https://github.com/bevacqua/dragula/pull/225/commits/b604c08378bb8ec878ff409f0591f63b774cfd81

I edited the last dragula.js version (3.6.8) and added this code before startBecauseMouseMoved function:

function lift (el) {
                var grabbed = _grabbed = canStart(el);
                if (!grabbed) {
                    return;
                }
                _offsetX = _offsetY = 0; // we could calc these on mousemove but 0,0 is simpler
                startOnLift();
            }

            function startOnLift () {
                var grabbed = _grabbed; // call to end() unsets _grabbed
                eventualMovements(true);
                movements();
                end();
                start(grabbed);
                classes.add(_copy || _item, 'gu-transit');
                renderMirrorImage();
            }

and lift: lift, before cancel: cancel,.

Also, add this in your JavaScript :

document.body.addEventListener('mousedown', function (e) {
       setTimeout(function(){
            if(e.target.hasClass('slide')){
                lifted = true;
                drake.lift(e.target);
            }
        }, 1000);
        });

and as a dragula option :

moves: function(el, source, handle, sibling) {
                if (lifted) { // Manage the drag after 1 second
                    lifted = false;
                    return true;
                }
            }

Don't forget to initialize dragula like this: var drake = dragula(containers, options);

I don't know why @bevacqua removed this from the API, it's working completely fine on my side 👍 .

linearza commented 8 years ago

Great stuff thanks @zetura ! Ill give it a shot once I dev this further, I've also forked dragula and started customizing the code.

skitterm commented 8 years ago

Delay is crucial for my workflow as well. Can it be reinstated as an option?

xsegrity commented 8 years ago

I need it as well

patrickfatrick commented 8 years ago

@zetura I haven't played with that code but what do you think about submitting a pull request on that? Seems like plenty of folks want this functionality.

linearza commented 8 years ago

I quickly created a PR for this using the logic @zetura provided - I still need to test it properly

regislutter commented 8 years ago

@patrickfatrick @linearza I'm actually looking to implement the functionality as a unique option (liftDelay). I just have an issue that if you wait and then scroll, you have the scroll + the mirror object. I think that @bevacqua removed the functionality because you need to put a lot of code in your implementation (that's why i'm looking to have a single option). :)

regislutter commented 8 years ago

@patrickfatrick @linearza Take a look at this commit : https://github.com/zetura/dragula/commit/d2465242cef4c84c136bd72763fb7128af1ecbd6 Let me know if it works for you. I can do a pull request if it's fine for you 👍

You just have to put the option "liftDelay" for this to work: var drake = dragula(lists, { revertOnSpill: true, direction: 'horizontal', liftDelay: 700 });

linearza commented 8 years ago

Aha - I havent worked on this in a while - but I just remembered why this implementation wasnt working for us. In our case we need to trigger a lift on press - but dragula is tightly coupled with its own gestures and the mapping of mouse to touch events. This causes a problem in our case since _grabbed doesnt get set when our press event (handled by hammerjs) gets fired. So really - what we need is to be able to trigger and maybe explicitly set the _grabbed element based on whatever function we choose to call. Ill see if I can figure something out as well

xsegrity commented 8 years ago

@zetura I tried out your change on a new ipad and it almost works but not quite. It seems to be jammed up with a scroll event where the item is trying to drag and the container is trying to scroll all at the same time making it kinda unusable.

My setup has multiple container columns that scroll horizontally off the screen (horz scroll on the columns parent container). Each column has zero or many vertically stacked items (a column can be vert scroll if it overflows). I tap and hold an item to be dragged, wait for the lift to kick in and then start dragging around. Although the drag kinda works it seems to hook with the scroll and as I drag, the container(s) are scrolling as well or at least attempting to.

I'm glad you gave this a shot though. I don't see anything inherently wrong with the code and it is how I would have gone about it as well. I can't think of a different approach right now but if I do I will chime back in on this thread. Happy to give other ideas a whirl to.

Oh, and I did discover a kinda workaround to the whole thing. On an iPad/iPhone you can simply do a 2 finger scroll to scroll the containers. Dunno about other touch devices though.

ben-girardet commented 8 years ago

In case it helps anyone, I've been able to manage this problem by exposing the grab method from the dragula API.

I've created a pull request for this change and written a short explanation of my workaround. You can read it here: https://github.com/bevacqua/dragula/pull/375

haschu commented 8 years ago

I had the same problem and came up with the following solution, perhaps this is helpful for others.

Since in 3.0.0 drag events won't start if mousemove or touchmove aren't fire, you could solve this issue by using stopPropagation. Here is an angular directive I wrote (should be easy to apply to vanilla.js - you get the idea). It uses a 1000ms press delay after which the dragging starts:

return {
    restrict: "A",
    link: function( scope, element ) {

        var touchTimeout,
            draggable = false;

        element.on( "touchmove mousemove", function( e ) {
            if ( !draggable ) e.stopPropagation();
        });

        element.on( "touchstart mousedown", function( event ) {
            touchTimeout = $timeout( function() {
                draggable = true;
            }, 1000 );
        });

        element.on( "touchend mouseup", function( event ) {
            $timeout.cancel( touchTimeout );
            draggable = false;
        });

    }
};
cormacrelf commented 7 years ago

@haschu's solution worked pretty well for me, with some modifications. Mainly, dragging before the delay timer makes it 'draggable' will then cancel the timer, which solves an issue where a mouse dragging the item would appear to have no effect at first, and then further mousemoves would have the item 'catch up'.

And this one's for Angular 2 and TypeScript, also obviously it's enabled only for touch devices.

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({ selector: '[delayDragLift]' })
export class DelayDragLiftDirective {

    dragDelay: number = 200; // milliseconds
    draggable: boolean = false;
    touchTimeout: NodeJS.Timer;

    @HostListener('touchmove', ['$event'])
    // @HostListener('mousemove', ['$event'])
    onMove(e: Event) {
        if (!this.draggable) {
            e.stopPropagation();
            clearTimeout(this.touchTimeout);
        }
    }

    @HostListener('touchstart', ['$event'])
    // @HostListener('mousedown', ['$event'])
    onDown(e: Event) {
        this.touchTimeout = setTimeout(() => {
            this.draggable = true;
        }, this.dragDelay);
    }

    @HostListener('touchend', ['$event'])
    // @HostListener('mouseup', ['$event'])
    onUp(e: Event) {
        clearTimeout(this.touchTimeout);
        this.draggable = false;
    }

    constructor(private el: ElementRef) {
    }
}
<!-- in your template -->
<div class="some-draggable-div" delayDragLift></div>
rohan-deshpande commented 6 years ago

Have implemented this in an ES5 react project and I think it's broken in >= iOS 11.3, I cannot stop the propagation of the touchmove event, possibly related to #426

Class methods:

    handleTouchStart: function () {
      var _this = this;

      this.touchTimeout = setTimeout(function () {
        _this.isDraggable = true;
      }, this.dragDelay);
    },

    handleTouchMove: function (e) {
      if (!this.isDraggable) {
        // this doesn't seem to do anything
        e.stopPropagation();
      }
    },

    handleTouchEnd: function () {
      clearTimeout(this.touchTimeout);
      this.isDraggable = false;
    },

Within the render method:

<div
  className='draggable-item'
  onTouchMove={this.handleTouchMove}
  onTouchStart={this.handleTouchStart}
  onTouchEnd={this.handleTouchEnd}
>
  {this.props.children}
</div>

The call to stop the propagation of the event in handleTouchMove simply doesn't do anything and so dragging is never blocked. Quite frustrating! Right now I'm wondering if reverting to dragula@2.0.0 will solve this by getting the delay option back.

rohan-deshpande commented 6 years ago

Reverted to v2.1.1, added delay via the option and it now works. I’ve seen some PRs open to add this option back, imo this is a much cleaner API than having to manually stop the propagation of another event. Not sure why it was removed.

rohan-deshpande commented 6 years ago

aaaand have discovered that this breaks a all of my ordering flows. Back to the drawing board. Am now back on v3.7.2 and will continue to investigate how to get this working.

radenkozec commented 4 years ago

@rohan-deshpande @cormacrelf @haschu Tried your solution but could not make it work. I attached to mousemove and touchmove events on draggable item and call e.stopPropagation(); but dragApi.on('drag', (el) => { console.log('drag'); Still shows that drag is performed and shadow is shown. Am I missing something? Thanks

haschu commented 4 years ago

@radenkozec Sorry, my workaround is almost 4 years old and I've never used dragula since... 😕

TotoTN commented 3 years ago

any updates on this issue ?

DarthSonic commented 1 year ago

any way to use delay in newest dragula?