본문 바로가기
기타/툴 관련(윈도우&리눅스)

쓸만한 Jquery Drag And Drop

by WebHack 2013. 1. 30.

개인적으로 바로 밑에 있는 링크가 가장 좋은 듯 

http://threedubmedia.com/code/event/drag/#demos

=========================================================================

Drag n Drop panels are great to let the user control how he/she wants to see the information as he can arrange various information blocks according to his preference. This type of functionality is often provided by web portals or personal homepage services like iGoogle.
iGoogle
Similarly, WordPress dashboard also lets user control how the various boxes are displayed.
WordPress Dashboard

Today, i am going to show you how to create collapsible, drag and drop panels easily using jQuery and jQuery UI libraries. Here’s what the final result will look like.

Collapsible Drag Drop Panels

Let’s begin with it.

Include The Libraries

We’ll be using the all powerful jQuery library and the jQuery UI libraries, so grab the latest version of both and add it to your page header.

The HTML Structure

We’ll be using two vertical columns defined by div with class="column" in this example that will hold our panels. You can increase the number of panels by adjusting the width in the stylesheet for.column. Each panel is a div with class="dragbox". And the content of each panel resides within div with class="dragbox-content".

Here’s the complete HTML structure for our example, i will be using 5 panels within two columns.

  1. <div class="column" id="column1">  
  2.     <div class="dragbox" id="item1" >  
  3.         <h2>Handle 1</h2>  
  4.         <div class="dragbox-content" >  
  5.             <!-- Panel Content Here -->   
  6.         </div>  
  7.     </div>  
  8.     <div class="dragbox" id="item2" >  
  9.         <h2><span class="configure" ><a href="#" >Configure</a></span>Handle 2</h2>  
  10.         <div class="dragbox-content" >  
  11.             <!-- Panel Content Here -->  
  12.         </div>  
  13.     </div>  
  14.     <div class="dragbox" id="item3" >  
  15.         <h2>Handle 3</h2>  
  16.         <div class="dragbox-content" >  
  17.             <!-- Panel Content Here -->  
  18.         </div>  
  19.     </div>  
  20. </div>  
  21. <div class="column" id="column2" >  
  22.     <div class="dragbox" id="item4" >  
  23.         <h2>Handle 4</h2>  
  24.         <div class="dragbox-content" >  
  25.             <!-- Panel Content Here-->  
  26.         </div>  
  27.     </div>  
  28.     <div class="dragbox" id="item5" >  
  29.         <h2>Handle 5</h2>  
  30.         <div class="dragbox-content" >  
  31.             <!--Panel Content Here-->   
  32.         </div>  
  33.     </div>  
  34. </div>  

You can add a link to configuration of a panel within the <h2> tag using <span class="configure">. This will show up when mouse hovers over the panel header.

Update
Each column now has a unique id and every panel also has a unique id, this will help in saving the order of panels if the user changes them.

The CSS Styles

Here are the CSS styles, you can modify it as you wish but remember to adjust the width of.column in case you increase the number of columns.

  1. .column{  
  2.     width:49%;  
  3.     margin-right:.5%;  
  4.     min-height:300px;  
  5.     background:#fff;  
  6.     float:left;  
  7. }  
  8. .column .dragbox{  
  9.     margin:5px 2px  20px;  
  10.     background:#fff;  
  11.     position:relative;  
  12.     border:1px solid #ddd;  
  13.     -moz-border-radius:5px;  
  14.     -webkit-border-radius:5px;  
  15. }  
  16. .column .dragbox h2{  
  17.     margin:0;  
  18.     font-size:12px;  
  19.     padding:5px;  
  20.     background:#f0f0f0;  
  21.     color:#000;  
  22.     border-bottom:1px solid #eee;  
  23.     font-family:Verdana;  
  24.     cursor:move;  
  25. }  
  26. .dragbox-content{  
  27.     background:#fff;  
  28.     min-height:100pxmargin:5px;  
  29.     font-family:'Lucida Grande'Verdanafont-size:0.8em; line-height:1.5em;  
  30. }  
  31. .column  .placeholder{  
  32.     background#f0f0f0;  
  33.     border:1px dashed #ddd;  
  34. }  
  35. .dragbox h2.collapse{  
  36.     background:#f0f0f0 url('collapse.png'no-repeat top rightright;  
  37. }  
  38. .dragbox h2 .configure{  
  39.     font-size:11pxfont-weight:normal;  
  40.     margin-right:30pxfloat:rightright;  
  41. }  

The JavaScript Code

To make the boxes draggable and droppable, we’ll be using the Sortables function of jQuery UI library. Here’ the code you need to use, add this to $(document).ready() function:

  1. $('.column').sortable({  
  2.     connectWith: '.column',  
  3.     handle: 'h2',  
  4.     cursor: 'move',  
  5.     placeholder: 'placeholder',  
  6.     forcePlaceholderSize: true,  
  7.     opacity: 0.4,  
  8. })  
  9. .disableSelection();  

Explanation: I used some options with sortable function . the first one connectWith lets you move panels across different columns. The handle defines the tag which is used to drag the panel. Theplaceholder parameter is the CSS class to use for the panel placeholder which is displayed when you drag a panel to show the possible position of the dragged panel. forcePlaceholderSize makes sure that placeholder size is equal to the size of the dragged panel and at last opacity sets the opacity of the panel while dragging.

Collapsing The Panels
To collapse the content of a panel when you click on the panel header and show the configure link on panel header on hover, use the below code in jQuery document ready function.

  1. $('.dragbox').each(function(){  
  2.     $(this).hover(function(){  
  3.         $(this).find('h2').addClass('collapse');  
  4.     }, function(){  
  5.         $(this).find('h2').removeClass('collapse');  
  6.     })  
  7.     .find('h2').hover(function(){  
  8.         $(this).find('.configure').css('visibility''visible');  
  9.     }, function(){  
  10.         $(this).find('.configure').css('visibility''hidden');  
  11.     })  
  12.     .click(function(){  
  13.         $(this).siblings('.dragbox-content').toggle();  
  14.     })  
  15.     .end()  
  16.     .find('.configure').css('visibility''hidden');  
  17. });  

One thing you’ll notice after adding this code is that when you drag the panel, the content of the panel toggles as when you drag the click event of panel header also fires that toggles the state of content. To fix this, you need to add another parameter to sortable so that when drag is finished, state of content does not toggle.

  1. $('.column').sortable({  
  2.     connectWith: '.column',  
  3.     handle: 'h2',  
  4.     cursor: 'move',  
  5.     placeholder: 'placeholder',  
  6.     forcePlaceholderSize: true,  
  7.     opacity: 0.4,  
  8.     stop: function(event, ui){  
  9.         $(ui.item).find('h2').click();  
  10.     }  
  11. })  
  12. .disableSelection();  

Here i added another parameter stop that defines the function to use when drag is complete. Once the drag is complete, click event of panel header is fired so that the original state of content is restored.

Saving State

Update (Sep 16, 2009)
Many of the commentors below asked about how to save the state of panels. I have written a follow up post on how to save state using database. Be sure to check it if you want to know.

There you are with nice multi-column collapsible drag drop panels.

See the Working Demo or Download source code & try.


drag-drop-panels.zip


==============================================================================


In the last couple of weeks I've started working with AngularJS. AngularJS is a great simple framework that allows you to create complete web-apps in javascript. It provides data-binding, templating and a lot of helper functions to create maintainable, readable and extendable browser based web applications. Lets first start with a disclaimer, I'm not an AngularJS guru, so some of the things here could probably have been done better, so if you have any comments, please let me know.

What I want to show in this article is how to create a directive that allows you to create simple drag and drop lists. This was something I needed for an application, but couldn't find any good, complete and working examples for. There are some examples out there, but mostly for older versions of AngularJS. In this article we'll look at the two following scenarios:

  1. Drag and drop within a single list: Probably the most common example, where you want to reorder an existing list by dragging items.
  2. Drag and drop from one list to another: Here we have one list that contains items that we want to drag and drop into a second list.

Single Drag and Drop example.png

If you want to go directly to the examples:

  1. Drag and Drop within a single list
  2. Drag and Drop from one list to another

Lets look at the code for these examples.

Drag and drop within a single list

We'll start with the easy one, drag and drop within a single list. We'll start with the html:

01.<!DOCTYPE html>
02.<html ng-app="dnd">
03.<body>
04.<div class="container" id="main"  ng-controller="dndCtrl">
05.<div class="row">
06.<div class="span4 offset4">
07.<ul id="single" dnd-list="model">
08.<li class="alert alert-info nomargin"
09.ng-repeat="item in model">{{item.value}}</li>
10.</ul>
11.</div>
12.</div>
13.</div>
14. 
15. 
16.<!-- load all the scripts -->
17.<script src="js/jquery-1.8.2.js" type="text/javascript"></script>
18.<script src="js/jquery-ui-1.9.2.custom.min.js" type="text/javascript"></script>
19.<script src="js/angular.js" type="text/javascript"></script>
20.<script src="js/bootstrap.min.js" type="text/javascript"></script>
21. 
22.<!-- load the app scripts -->
23.<script src="app/app.js" type="text/javascript"></script>
24.<script src="app/dir-dnd.js" type="text/javascript"></script>
25.<script src="app/ctrl-dnd.js" type="text/javascript"></script>
26. 
27.</body>
28.</html>

In this page we define a couple of AngularJS specifics. First we use "ng-app=dnd" to define the name of our application and we define an AngularJS controller with the name "dndCtrl". You can also see that we create a list of items using "ng-repeat". Last, but not least, we've defined a custom directive "dnd-list" to handles the drag and drop functionality. Before we look at this directive, lets quickly look at the the application (app.js) and the controller.

App.js:

1.var app = angular.module('dnd', []);

Nothing special, we just define the application.

The controller (ctrl-dnd.js):

01.function dndCtrl($scope) {
02. 
03.$scope.model = [
04.{
05."id": 1,
06."value""Who the fuck is Arthur Digby Sellers?"
07.},
08.{
09."id": 2,
10."value""I've seen a lot of spinals, Dude, and this guy is a fake. "
11.},
12.{
13."id": 3,
14."value""But that is up to little Larry here. Isn't it, Larry?"
15.},
16.{
17."id": 4,
18."value"" I did not watch my buddies die face down in the mud so that this fucking strumpet."
19.}
20.];
21. 
22.// watch, use 'true' to also receive updates when values
23.// change, instead of just the reference
24.$scope.$watch("model"function(value) {
25.console.log("Model: " + value.map(function(e){returne.id}).join(','));
26.},true);

Also nothing special, just a dummy model, and a watch function to see if our drag and drop functionality works. The only item left is our drag and drop directive.

01.// directive for a single list
02.app.directive('dndList'function() {
03. 
04.return function(scope, element, attrs) {
05. 
06.// variables used for dnd
07.var toUpdate;
08.var startIndex = -1;
09. 
10.// watch the model, so we always know what element
11.// is at a specific position
12.scope.$watch(attrs.dndList, function(value) {
13.toUpdate = value;
14.},true);
15. 
16.// use jquery to make the element sortable (dnd). This is called
17.// when the element is rendered
18.$(element[0]).sortable({
19.items:'li',
20.start:function (event, ui) {
21.// on start we define where the item is dragged from
22.startIndex = ($(ui.item).index());
23.},
24.stop:function (event, ui) {
25.// on stop we determine the new index of the
26.// item and store it there
27.var newIndex = ($(ui.item).index());
28.var toMove = toUpdate[startIndex];
29.toUpdate.splice(startIndex,1);
30.toUpdate.splice(newIndex,0,toMove);
31. 
32.// we move items in the array, if we want
33.// to trigger an update in angular use $apply()
34.// since we're outside angulars lifecycle
35.scope.$apply(scope.model);
36.},
37.axis:'y'
38.})
39.}
40.});

As you can see from the code, it isn't that complex. We have a single watch function that's called whenever our model changes and store the updated model in a local variable. Next we use the JQuery UI sortable function, to enable drag and drop for our element. All we need to do know is make sure our backing model is kept consistent with the state on screen. For this we use the "start" and "stop" properties passed into the sortable function.
In start we keep track of the element that was dragged, and in stop we push the element back at the changed position in the array. The last step we need to take is that we have to inform angular of this change. We're working outside the lifecycle of angular so with scope.$apply we inform angular that it should re-evaluate the passed in expression. All this together looks like this (look at the console output to see the output from the watch function in our controller):

Single Drag and Drop example-1.png

Next we'll look at the changes you need to make to allow elements to be dragged between lists.

Drag and Drop from one list to another

The HTML for this looks pretty much the same, the only thing that is changed is that we now have two lists, with a slightly different attribute.

01.<div class="row">
02.<div class="span4 offset2">
03.<ul id="sourceList" dnd-between-list="source,targetList">
04.<li class="alert alert-error nomargin"
05.ng-repeat="item in source">{{item.value}}</li>
06.</ul>
07.</div>
08.<div class="span4">
09.<ul id="targetList" dnd-between-list="model,sourceList">
10.<li class="alert alert-info nomargin"
11.ng-repeat="item in model">{{item.value}}</li>
12.</ul>
13.</div>
14.</div>

What you see here is that besides passing in the model the list is working on, we also pass in the list to which it is connected. In a bit we'll see how this is used. First though let's look at our updated controller:

01.function dndCtrl($scope) {
02. 
03.$scope.model = [
04.{
05."id": 1,
06."value""Who the fuck is Arthur Digby Sellers?"
07.},
08.{
09."id": 2,
10."value""I've seen a lot of spinals, Dude, and this guy is a fake. "
11.},
12.{
13."id": 3,
14."value""But that is up to little Larry here. Isn't it, Larry?"
15.},
16.{
17."id": 4,
18."value"" I did not watch my buddies die face down in the mud so that this fucking strumpet."
19.}
20.];
21. 
22.$scope.source = [
23.{
24."id": 5,
25."value""What do you mean \"brought it bowling\"? I didn't rent it shoes."
26.},
27.{
28."id": 6,
29."value""Keep your ugly fucking goldbricking ass out of my beach community! "
30.},
31.{
32."id": 7,
33."value""What the fuck are you talking about? I converted when I married Cynthia!"
34.},
35.{
36."id": 8,
37."value""Ja, it seems you forgot our little deal, Lebowski."
38.}
39.];
40. 
41.// watch, use 'true' to also receive updates when values
42.// change, instead of just the reference
43.$scope.$watch("model"function(value) {
44.console.log("Model: " + value.map(function(e){returne.id}).join(','));
45.},true);
46. 
47.// watch, use 'true' to also receive updates when values
48.// change, instead of just the reference
49.$scope.$watch("source"function(value) {
50.console.log("Source: " + value.map(function(e){returne.id}).join(','));
51.},true);
52.}

Not much has changed, we only added another simpel array we use as input for our source list, and added another listener that shows the content of that list on change. The only other thing we changed is that we added a new directive.

01.// directive for dnd between lists
02.app.directive('dndBetweenList'function($parse) {
03. 
04.return function(scope, element, attrs) {
05. 
06.// contains the args for this component
07.var args = attrs.dndBetweenList.split(',');
08.// contains the args for the target
09.var targetArgs = $('#'+args[1]).attr('dnd-between-list').split(',');
10. 
11.// variables used for dnd
12.var toUpdate;
13.var target;
14.var startIndex = -1;
15.var toTarget = true;
16. 
17.// watch the model, so we always know what element
18.// is at a specific position
19.scope.$watch(args[0], function(value) {
20.toUpdate = value;
21.},true);
22. 
23.// also watch for changes in the target list
24.scope.$watch(targetArgs[0], function(value) {
25.target = value;
26.},true);
27. 
28.// use jquery to make the element sortable (dnd). This is called
29.// when the element is rendered
30.$(element[0]).sortable({
31.items:'li',
32.start:function (event, ui) {
33.// on start we define where the item is dragged from
34.startIndex = ($(ui.item).index());
35.toTarget = false;
36.},
37.stop:function (event, ui) {
38.var newParent = ui.item[0].parentNode.id;
39. 
40.// on stop we determine the new index of the
41.// item and store it there
42.var newIndex = ($(ui.item).index());
43.var toMove = toUpdate[startIndex];
44. 
45.// we need to remove him from the configured model
46.toUpdate.splice(startIndex,1);
47. 
48.if (newParent == args[1]) {
49.// and add it to the linked list
50.target.splice(newIndex,0,toMove);
51.}  else {
52.toUpdate.splice(newIndex,0,toMove);
53.}
54. 
55.// we move items in the array, if we want
56.// to trigger an update in angular use $apply()
57.// since we're outside angulars lifecycle
58.scope.$apply(targetArgs[0]);
59.scope.$apply(args[0]);
60.},
61.connectWith:'#'+args[1]
62.})
63.}
64.});

Not that much different from the previous one. But a couple of things have changed. First we parse the arguments from our own list and from the target list. We do this so we know which items in our controller we need to update:

1.// contains the args for this component
2.var args = attrs.dndBetweenList.split(',');
3.// contains the args for the target
4.var targetArgs = $('#'+args[1]).attr('dnd-between-list').split(',');

We also add an additional watch so we always have the correct variables whenever these lists change in the backend. Finally, when we look at the sortable function, not that much is changed. We use an additional "connectWith" property to tie these two lists together, based on the supplied arguments to the directive. We've also needed to change our "stop" function. Instead of removing and reinserting the item in the same array, we remove it from the source one, and add it to the target one.

01.stop:function (event, ui) {
02.var newParent = ui.item[0].parentNode.id;
03. 
04.// on stop we determine the new index of the
05.// item and store it there
06.var newIndex = ($(ui.item).index());
07.var toMove = toUpdate[startIndex];
08. 
09.// we need to remove him from the configured model
10.toUpdate.splice(startIndex,1);
11. 
12.if (newParent == args[1]) {
13.// and add it to the linked list
14.target.splice(newIndex,0,toMove);
15.}  else {
16.toUpdate.splice(newIndex,0,toMove);
17.}
18. 
19.// we move items in the array, if we want
20.// to trigger an update in angular use $apply()
21.// since we're outside angulars lifecycle
22.scope.$apply(targetArgs[0]);
23.scope.$apply(args[0]);
24.}

As you can see, all we do here is determine whether it was dropped on the specified target, or whether it was moved in the list itself. Based on this it is added to the correct model, and the models are updated. The output in the console looks like this:

Single Drag and Drop example-2.png

We're almost there, there is just one thing we need to take care off. What happens when one of our lists is empty? That would mean we can't drop an item back from one list to another, since we can't really see the empty list. To solve this we need to make sure, the empty list (the ul element) has a forced height. To do this we use the ng-class attribute, this attribute allows us to conditionally add a class based on the state of our model. To do this, first add the following helper methods to the controller:

1.$scope.sourceEmpty = function() {
2.return $scope.source.length == 0;
3.}
4. 
5.$scope.modelEmpty = function() {
6.return $scope.model.length == 0;
7.}

Next update the html with the list definition to this:

01.<div class="span4 offset2">
02.<ul id="sourceList"
03.dnd-between-list="source,targetList"
04.ng-class="{'minimalList':sourceEmpty()}">
05.<li class="alert alert-error nomargin"
06.ng-repeat="item in source">{{item.value}}</li>
07.</ul>
08.</div>
09.<div class="span4">
10. 
11.<ul id="targetList"
12.dnd-between-list="model,sourceList"
13.ng-class="{'minimalList':sourceEmpty()}">
14.<li class="alert alert-info nomargin"
15.ng-repeat="item in model">{{item.value}}</li>
16.</ul>
17.</div>

You can see we added a ng-class attribute that adds the minimalList class whenever a list is empty. The last thing we need to add is this minimalList style, which looks like this:

1..minimalList {
2.min-height: 100px;
3.}

And now, we can also drag and drop from one list to another, when either one is empty! Simple right? You should be able to use this without changing much.