Jump to content
Search Community

How to create a sortable list with Draggable

OSUblake test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

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 14
  • Thanks 1
Link to comment
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 9
Link to comment
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 ?editors=0010 by osublake (@osublake) on CodePen

  • Like 7
Link to comment
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 ?editors=0010 by osublake (@osublake) on CodePen

  • Like 2
Link to comment
Share on other sites

  • 2 months later...
  • 2 weeks later...

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 comment
Share on other sites

  • 3 weeks later...

Hello guys!

 

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

See the Pen ?editors=0010 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 comment
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 comment
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 comment
Share on other sites

  • 3 years later...

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 comment
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 comment
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.
×
×
  • Create New...