kornelski / slip

Slip.js — UI library for manipulating lists via swipe and drag gestures
BSD 2-Clause "Simplified" License
2.44k stars 213 forks source link

Locked elements in lists #96

Closed mariofagac closed 6 years ago

mariofagac commented 6 years ago

Hi, Is it possible to lock the positions of one or more elements in the list so that other elements slip around them? That would be a great feature.

kornelski commented 6 years ago

You can prevent some elements from being moved by calling preventDefault() on all operations (see how the example page does it).

The library doesn't have a concept of empty space between elements, so if an element is removed, it has no choice but move other elements to fill the gap.

mariofagac commented 6 years ago

Thanks.

mstobin commented 6 years ago

The suggested solution only solves part of the problem though, because it still allows items to move past an object.

Let's say I have a list of 10 items, where the top and bottom item should be fixed in place, and the other 8 items moveable.

We can block the top and bottom item from being dragged, but nothing stops the user dragging item 5 to the top of the list, even though the top item itself cannot be dragged.

In the end, the outcome is the same - the list ends up in an invalid order.

It would be great if there was a way to mark items that should not participate in the drag and drop at all, and not allow them to be shuffled out of place. Ideally, when the user hits one such boundary, the item being dragged would "stick" to this boundary too rather than proceeding past it.

The Sortable.js library does this with an option to set a selector for which items are draggable, and only those items participate.

Unfortunately, I ran into some bugs with that library on iOS 11, which is why I am looking to swap across to slip.js. This library was quick to implement and works perfectly, except for this one requirement.

I can roll up my sleeves and get dirty in the code if required - but any pointers on where to look?

kornelski commented 6 years ago

Ah, I see. That makes sense.

There's reordering state:

https://github.com/pornel/slip/blob/master/slip.js#L371

and other elements are visually moved here:

https://github.com/pornel/slip/blob/master/slip.js#L416

and final order is decided here:

https://github.com/pornel/slip/blob/master/slip.js#L468

So in these places you'd have to figure out how to skip fixed-position elements.

mstobin commented 6 years ago

Ok, thanks. I have something that works for my purpose.

I haven't really been involved in Open Source projects or Github, so I have no idea how to submit Pull Requests etc. if you want to add this into the main library.

But if you are interested in the changes, all I needed to do was the following:

CODE CHANGES

Inside function Slip(): Add the following underneath existing options handling:

this.options.moveableClass = options.moveableClass || null;

Inside reorder function: Where otherNodes is first being created, add the moveable variable:

otherNodes.push({
    // ... as before
    moveable: this.options.moveableClass ? 
        nodes[i].classList.contains(this.options.moveableClass) : true
});

Inside both reorder: onMove(): and leaveState():

otherNodes.forEach(function(o){
    if (!o.moveable) { return; }
    // ... as before
});

Inside onEnd(): Change the following line:

if (otherNodes[i].pos > move.y) {

To this (checks for moveable):

if (otherNodes[i].moveable && otherNodes[i].pos > move.y) {

And likewise, for the next loop, change:

if (otherNodes[i].pos < move.y) {

To this:

if (otherNodes[i].moveable && otherNodes[i].pos < move.y) {

USAGE With those changes in place, usage is pretty straightforward.

When creating the Slip instance, specify the class name that will be used for moveable elements:

var slipInstance = new Slip(domEl, { moveableClass: 'moveable' });

And then when creating your list (either in HTML or dynamically), make sure that moveable elements have that class:

<ul>
<li>Fixed at top</li>
<li class="moveable">Item 1</li>
<li class="moveable">Item 2</li>
<li class="moveable">Item 3</li>
<li>Fixed at bottom</li>
</ul>

IMPROVEMENTS

Tested on top and bottom only I have tested this code on items that cannot be moved from the top and the bottom of the list, which was my use case.

I have not tested what would happen with items that cannot be moved in the middle of the list... but then, I am not sure what logically should happen either in that case.

Before Reorder I didn't bother, since I already handled it elsewhere in the existing callbacks, but it may make sense to adjust how/when slip:beforereorder is called to check for the moveable flag and automatically block movement rather than relying on the callback.

Right now, I still have to handle it by calling e.preventDefault() in the slip:beforereorder callback, which seems like unnecessary double handling:

domEl.addEventListener('slip:beforereorder', function(e) {
    if (!e.srcElement.classList.contains('moveable')) { e.preventDefault(); }
}

Compatibility I am not 100% sure of the backwards compatibility of domEl.classList.contains(), and how far back you are trying to aim, so you may need to perform this check in a different way. I know there are more backwards-compatible options floating around the internet.

carter-thaxton commented 6 years ago

@mstobin This seems like a great first pull request, if you're interested in getting involved.

Check out https://yangsu.github.io/pull-request-tutorial/ or https://help.github.com/articles/creating-a-pull-request/

kornelski commented 6 years ago

The change seems simple enough. I'd recommend keeping elements movable by default (with no class) and using class only to make them unmovable.

Perhaps the class could be slip-fixed-header or slip-fixed-edge, so that it's clear it won't work in the middle.