Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
OSUblake

How to create a sortable list with Draggable

Recommended Posts

I keep getting a lot of questions asking about creating sortable lists with Draggable, so I'm just going to make a post about it.

 

My

See the Pen RNLdpz by osublake (@osublake) on CodePen

example is outdated, and I no longer use that technique, so I'm not going to update it. It relies on hit testing, which I would no longer recommend doing for grids/lists. A better way is to create a model of your list, and map the location of your draggable to that model.

 

This is real easy to do using arrays. You can use the index of an array to represent a cell location in the grid. Draggable already creates an array for you if you use the .create() method, so you could use that if you really wanted to.

 

But first you need to figure out the size of the cells in your list. If everything is uniform, you could probably just divide the size by the number of items in that dimension. If my list is 400px high, and there are 4 elements, the cell size for a row is 100. Notice how the cell size may not be the same size as your element. The cells are in red.

 

9kORfXv.png

 

 

When you drag, you can figure out the index of a cell like this. 

var index = Math.round(this.y / rowSize);

This can result in errors if you drag outside of your list, so you should clamp the values like this.

var index = clamp(Math.round(this.y / rowSize), 0, totalRows - 1);

function clamp(value, a,  {
  return value < a ? a : (value > b ? b : value);
}

Now you can easily determine where everything is. 

 

You also need to keep track of the last index, and compare it to the index you calculate while dragging. If they are different, this means your draggable is inside a different cell, so you need to update your layout.

 

Before you can change the layout, your first need to change the items in your array. Here's a function you can use to move stuff around in an array.

arrayMove(myArray, oldIndex, newIndex);

function arrayMove(array, from, to) {
  array.splice(to, 0, array.splice(from, 1)[0]);
}

Now that your array is in the correct order, you can update the layout. If you were using an array of draggables, you could animate the entire layout like this.

myArray.forEach((draggable, index) => {
  if (!draggable.isDragging) {
    TweenLite.to(draggable.target, 0.4, { y: index * rowSize });
  }
});

That's pretty much it!

 

Doing it this way is not only easier, but it performs a lot better, making it really smooth. I made a demo based off of this Framer.js example. It's about 100 lines of code, and is pretty easy to understand. For comparison, The Framer.js example is about 180 lines of code.

See the Pen jrqjdy by osublake (@osublake) on CodePen

  • Like 12
  • Thanks 1
Link to post
Share on other sites

That is great Blake! 

 

I just wanted to take a moment and thank you for hanging out here on the forum. I can honestly say I've learned just as much (if not more) by reading your posts and deconstructing your pens than I have by reading books and viewing online tutorials. You are truly an amazingly talented coding rock star and we're lucky to have you as an inspirational teacher around here. 

 

Thank you Blake.

 

:)

  • Like 8
Link to post
Share on other sites

Yeah, great stuff Blake. Thanks for sharing this. 

Link to post
Share on other sites

Thanks PointC! That's how I learn any new concept. I find something interesting, and deconstruct the code line-by-line. This is pretty much the only book you'll ever need.

 

R9qBSxo.png

 

  • Like 7
Link to post
Share on other sites

I should add that converting a list to a grid is real easy. 

 

First, map the cell locations to an array.

var cells = [];

for (var row = 0; row < totalRows; row++) {
  for (var col = 0; col < totalCols; col++) {
    cells.push({
      x: col * colSize,
      y: row * rowSize
    });
  }
}

Now modify the drag method to find the index of a cell.

var col = clamp(Math.round(this.x / colSize), 0, totalCols - 1);
var row = clamp(Math.round(this.y / rowSize), 0, totalRows - 1);
       
var index = totalCols * row + col;

Now you can easily layout the grid using the saved cell locations. Everything else is pretty much the same!

See the Pen NrRJwm by osublake (@osublake) on CodePen

  • Like 7
Link to post
Share on other sites

Blake...

 

I WANT YOUR CHILDREN!

 

Think of the possibilities of such talented DNA. :D

  • Like 3
Link to post
Share on other sites

That's actually not a bad idea. Do you know any filmmakers? I'd like to document the process.

  • Haha 1
Link to post
Share on other sites

Pedro,

 

I've never been so extremely horrified yet equally humored at the same time.

 

  :-o  :-o  :mrgreen:  :-P

  • Like 3
Link to post
Share on other sites

He's the one who wants to film it. I'm not so keen in this new ideas the youngster have but hey, Blake's calling the shots on this one.

 

8-)

  • Like 1
Link to post
Share on other sites

This thread has taken an unusual turn. What is happening here? I'm amused and confused.  :lol:  :blink:  

 

I'll be in my lab inventing a time travel machine so I can somehow warn myself not to look at that picture. :shock:

Link to post
Share on other sites

I'm framing that!!!  :shock:  :-o  :-o  :lol:

 

On a side note, somebody suggested swapping positions vertically. Another easy modification. Instead of shifting the array around, you can swap positions in the array like this...

var temp = sortables[to];
sortables[to] = item;
sortables[item.index] = temp;

I changed it so that it will use the swap method if the new position is adjacent to the last position.

See the Pen NrRJwm by osublake (@osublake) on CodePen

  • Like 2
Link to post
Share on other sites

That's awesome! And thanks for sharing this. We get a lot of request asking how to make drag and drop containers, but I haven't had to time make another version, so this is perfect.

  • Like 1
Link to post
Share on other sites

JoelCox, Very nice! Thanks for sharing.

Link to post
Share on other sites

I am trying to set a bounds to the example you have done @OSUBlake, however it seems that I can't apply a bounds? If I do that, when I start dragging, the element will jump to a odd spot instantly? The reason I am looking at applying bounds is so that I can do things like:

throwProps: true,
edgeResistance: 0.9,
overshootTolerance: 0.1,
maxDuration: 0.2,

I don't want the draggable element to be able to drag to where ever but contained within an area with momentum applied.

 

Updates*

 

Ah, I tried again and it seems to work (:

Link to post
Share on other sites

Hello guys!

 

Exist any example with Angular 1.x using directives? I want to do that 

See the Pen jrqjdy by osublake (@osublake) on CodePen

but using Ionic Framework.

 

Using Ionic list component

<ion-list>
  <ion-item draggable ng-repeat="item in items">
    Hello, {{item}}!
  </ion-item>
</ion-list>

And using Angular directives =>

app.directive('draggable', ['$ionicGesture', function ($ionicGesture) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
          //Ionic list
          var container = element.parent()[0];

          var animation = TweenLite.to(element, 0.3, {
            boxShadow: "rgba(0,0,0,0.2) 0px 16px 32px 0px",
            force3D: true,
            scale: 1.1,
            paused: true
          });

          var dragger = new Draggable(element, {
            onPress: onPress,
            onDragStart: downAction,
            onRelease: upAction,
            onDrag: dragAction,
            cursor: "inherit",    
            type: "y"
          });


          //Any example please?
        }
    }
}]);

With the following code doesn't work well => https://gist.github.com/jdnichollsc/4c1ae672bfbad6bc364aa42f4dacd38e

 

And other PoC in Codepen

See the Pen kkvpwb?editors=1010 by jdnichollsc (@jdnichollsc) on CodePen

 

Thanks in advance, Nicholls  :mrgreen:

Link to post
Share on other sites

Hi jdnichollsc,

 

It looks you're off to a good start. I'm going to come back and look at this in more detail later today, but for now try adding autoScroll to your Draggable instance.

dragger = new Draggable(element, {
  autoScroll: 1,
  ...
});
BTW, I really like all the work you've done with Ionic, Angular, and Phaser. It's really impressive!
  • Like 2
Link to post
Share on other sites

Hi jdnichollsc,

 

It looks you're off to a good start. I'm going to come back and look at this in more detail later today, but for now try adding autoScroll to your Draggable instance.

dragger = new Draggable(element, {
  autoScroll: 1,
  ...
});
BTW, I really like all the work you've done with Ionic, Angular, and Phaser. It's really impressive!

 

 

 

Wowwww is beautiful!!

Thanks for your examples! Check the repo with the last fix https://jdnichollsc.github.io/Ionic-Drag-and-Drop/

 

What do you think?  :-P

 

 

Regards, Nicholls

Link to post
Share on other sites

Hi everyone, 
I have a problem implementing this method in GSAP 3
When I run the code I get this error message :  script.js:39 Uncaught TypeError: Cannot read property 'getComputedStyle' of undefined

 

Some idea?
thank you very much

 

here is line 39

   
    39      var dragger = new Draggable (element ,{    
    40      type: "y,x",
    41      lockAxis: true,
    42      edgeResistance: 0.5,
    43          bounds: ".container"
    44          onDragStart: dragStart,
    45          onDrag: whileDrag,
    46          onDragEnd: dragEnd
    47      });
 
Link to post
Share on other sites

Hm, it's tough to troubleshoot blind but did you register the Draggable plugin? 

 

gsap.registerPlugin(Draggable)

If you're still having trouble, please provide a reduced test case in codepen or something like that and we'd be happy to take a peek. 

  • Like 1
Link to post
Share on other sites

Thank you for the quick answer!
I'm using the Vanilla version.  This is my code:

 

HTML

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.6/gsap.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.6/Draggable.min.js"></script>
	<script src="script.js"></script>
	<title>::::::::::</title>
	
</head>
<style>
		*{
			font-family: sans-serif;
		}
		.list-item{
			padding: 20px ;
			width: 100%;
			background: #e0e0e0;
			position: absolute;
			top: 0;
			left: 0;  
			box-sizing: border-box;
		}
		.container{
			border: 1px solid red;
			width: 324px;
			position:absolute;
			left: 20PX;
			top:100px;
			box-sizing: border-box;
		}
</style>
<body>
	<div class="container">

			<div class="list-item">
				<div class="item-content">
					<strong>Alpha </strong><span class="order">0</span> 
				</div>
			</div>
			
			<div class="list-item">
				<div class="item-content">
					<strong>Bravo </strong><span class="order">1</span> 
				</div>
			</div>
			
			<div class="list-item">
				<div class="item-content">
					<strong>Charlie </strong><span class="order">2</span> 
				</div>
			</div>
			
			<div class="list-item">
				<div class="item-content">
					<strong>Delta </strong><span class="order">3</span> 
				</div>
			</div>
	</div>
</body>
</html>

 

JAVASCRIPT

'use strict';

var sortablesArray;
const gridHeight = 80;
var totalItemsCount;

document.addEventListener('DOMContentLoaded', ready);

function ready(){
	
	const items = Array.from(document.querySelectorAll('.list-item'));
	totalItemsCount = items.length;
	const container = document.querySelector('.container');
	container.style.height = items.length * gridHeight + "px";
	drag(items);
}


function drag(items){

	sortablesArray = items.map(sortableCreator);
	function sortableCreator(element, index) {
		console.log(element)
		var dragger = new Draggable (element ,{
			type: "y,x",
			lockAxis: true,
			edgeResistance: 0.5,
			bounds: ".container", 
			onDragStart: dragStart,
			onDrag: whileDrag,
			onDragEnd: dragEnd
		});

		const sortable = {
			'name': element.querySelector('STRONG').innerText,
			'dragger':dragger,
			'element':element,
			'index': index,
		};

		//posicionar inicialmente
		gsap.set(element, { y: index * gridHeight }); 

		const animation_OnDrag = gsap.to(element, {
			duration: 0.05,
			scale: 1.04,
			force3D: false,
			boxShadow: "rgba(0, 0, 0, 0.2) 0px 30px 50px -10px",
			paused: true,
			ease: 0.5
		});

		function dragStart() {
			console.log("start drag");
			animation_OnDrag.play();
		}

		function dragEnd() {
			console.log("end drag");
			animation_OnDrag.reverse();
			gsap.to(element, {duration:0.3, y: sortable.index * gridHeight });
		}

		function whileDrag() {
			var newIndex = clamp(Math.round(dragger.y / gridHeight), 0, totalItemsCount - 1);  // yPosition, minValue , maxValue
			if(sortable.index != newIndex){
				move(sortablesArray,sortable.index, newIndex);
				changePosition();
			}	
		}
		return sortable;
	}
}

function changePosition(){
sortablesArray.forEach((sortable, index) => {
	if (!sortable.dragger.isDragging) {	
		gsap.to(sortable.element,{duration: 0.3, y: sortable.index * gridHeight });
	}
});
}

function move(array, from, to) {
array.splice(to, 0, array.splice(from, 1)[0]);
array.forEach((el, index) => {
	el.index = index;
	el.element.querySelector('SPAN').innerText = index;
	});	
}

function clamp(value, min, max) {
	return value < min ? min : value > max ? max : value;
}

 

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×