Thursday, September 13th, 2007 by Rob
For a client today I had to create some functionality to take elements out of a list and place them into another list. Sounds pretty straight forward so I decided to build it using Ajax.
For the site I was working on I have been using the Scriptaculous JavaScript library which I’ve been quite happy with, even though their site may be a bit slow at times. Their Sortable functionality looked like it might do the job so I started to plug away in so test designs. However, there was a major problem. My lists were pretty long, and I wanted the ability to scroll. However, when I added an "overflow: scroll" attribute to the CSS, it all went a bit wrong. When I tried to drag an element out of a list to drop into another one, the scoll came into effect and I could not get it out of the list - instead the scroll bars became ever longer. Argh! I looked into it further and soon found that other people were having this problem:
http://wiki.script.aculo.us/scriptaculous/discuss/Sortable.create
I tried a number of fixes advised, but sadly to no avail. I also found Rico, which looked promising, especially this demo:
http://openrico.org/demos?demo=drag_and_drop_custom_draggable
but I didn’t really want to learn yet another library. So i turned to a library I’d been previously playing around with: jquery and it’s interface plugin.
In a relatively short amount of time I was able to get roughly what I wanted working well for Firefox and the overflow problem wasn’t a, er, problem!
Note: sadly IE doesn’t seem to work - but I’m not overly bothered about that at the moment as the client uses Firefox (as yes I know that’s bad practice but deadlines are looming, one day I’ll finish it off!)
The Howto
Seeming that I spent pretty much most of the morning tying to find something that did this I’m going to jot down how I did it in the hope that I can stop someone else pulling their hair out.
You can see a demo of the drag and drop here.
First thing is first, include the needed JavaScript files (I find that interface.js is a bit buggy with jquery 1.2 so I prefer to keep to 1.1.2
<script src="jquery-1.1.2.js" type="text/javascript"></script>
<script src="interface.js" type="text/javascript"></script>
Next I set up the styles, just to look nice and to scroll etc
<style type="text/css">
body {
font-family:Verdana, Arial, Helvetica, sans-serif;
}
#origdiv, #saveddiv {
float: left;
margin: 0 10px 10px 10px;
}
select {
font-size: 10px;
border: 1px solid #000;
width: 200px;
height: 200px;
overflow: auto;
}
.list1, list2 {
font-size: 10px;
padding: 5px 0;
border-bottom: 1px dashed #ccc;
cursor: pointer;
}
#origdiv select option, #saveddiv select option {
display: block;
padding: 5px 0;
border-bottom: 1px dashed #999;
z-index: 999999;
}
</style>
In the body I added the original list that I would be dragging things from. I chose to do it with <option>’s rather that <li>’s as I can easily extract the value from them. As you can see, each option has an ID which roughly matches its value - this is used later when removing the option from the list and putting it into the Saved List. Also, note that each option has been given a class of "list1", this will be used to determine if these elements are allowed to be dropped into the "saved list"
<div id="origdiv">
<h3>Original List</h3>
<select size="5">
<option class="list1" id="o1" value="1">Original List 1</option>
<option class="list1" id="o2" value="2">Original List 2</option>
<option class="list1" id="o3" value="3">Original List 3</option>
<option class="list1" id="o4" value="4">Original List 4</option>
<option class="list1" id="o5" value="5">Original List 5</option>
<option class="list1" id="o6" value="6">Original List 6</option>
<option class="list1" id="o7" value="7">Original List 7</option>
<option class="list1" id="o8" value="8">Original List 8</option>
<option class="list1" id="o9" value="9">Original List 9</option>
<option class="list1" id="o10" value="10">Original List 10</option>
<option class="list1" id="o11" value="11">Original List 11</option>
<option class="list1" id="o12" value="12">Original List 12</option>
<option class="list1" id="o13" value="13">Original List 13</option>
<option class="list1" id="o14" value="14">Original List 14</option>
<option class="list1" id="o15" value="15">Original List 15</option>
<option class="list1" id="o16" value="16">Original List 16</option>
<option class="list1" id="o17" value="17">Original List 17</option>
<option class="list1" id="o18" value="18">Original List 18</option>
<option class="list1" id="o19" value="19">Original List 19</option>
<option class="list1" id="o20" value="20">Original List 20</option>
</select>
</div>
And next was another select box where the dropped entries will end up
<div id="saveddiv">
<h3>Saved List</h3>
<select class="savedlist" id="savedlist" name="savedlist" size="5">
<!– list entries will end up here –>
</select>
</div>
Also, as select boxes only return what you have selected and not all items in the box, I have included this space to add hidden fields with the values of the dropped entries
<div id="hidden">
<p><small><em>When items are dragged onto the list, there values will display here.<br />
Normally these would be hidden fields</em></small></p></div>
</div>
Now for the JavaScript.
I created an array which will be looped through to get the values of the options. The reason I am looping through an array of values and not simply doing a for(i=0; i<20; i++) loop is because in the real version my options do not follow a numeric pattern so it is easier to create an array of values and loop through those than to manually create a draggable for each option. Thinking about it, I could loop through the actual options to get their values, but it’s late at night and I now can’t be bothered.
/*
* Create an array with which to init the draggable element
* Note, I could have looped though numbers 1-20 but I have done it
* this way so if you are using items that don’t follow a numeric
* pattern, it will still work
*/
o_array = new Array(
‘1′,’2′,’3′,’4′,’5′,’6′,’7′, ‘8′, ‘9′,’10′,’11′,’12′,’13′,’14′,’15′,’16′,’17′, ‘18′, ‘19′,’20′
);
Loop though the array to create a draggable element for each option
/*
* Using the array, loop through creating the draggable elements
*/
for(i=0; i<o_array.length; i++) {
var id = "#o"+o_array[i];
$(id).Draggable(
{
zIndex: 1000,
ghosting: true,
revert: true,
opacity: 0.7
}
);
}
Init the dropable area. Note the "accept: ‘list1′. This makes it possible to drop the options each of which, if you recall, have been given the class "list1". When something is dropped onto this it will fire up the "add_to_saved()" function passing the element - or option - that has been dropped on it
/**
* Create the droppable area
**/
$(’#savedlist’).Droppable(
{
accept : ‘list1′,
ondrop: function (drag)
{
add_to_saved(drag);
},
fit: true
}
);
Now all the initiation has been done, we can get on and write the actual function that removes the dragged option from the original list and adds it to the saved list, plus populating our form so we can submit it.
add_to_save() is called when an option is successfully dropped onto the Saved List select box and the element that is dropped onto it is passed - in our case an option element. From the value of the option element we can get its ID, which we then use to remove it from the scene. Once removed we now add it back, but this time into the Saved List, giving it a different class just to be sure we don’t confuse ourselves. So the list looks like it is all working, but stupidly when I began this I forgot that only the selected options of the select box are passed. To fix this I simply created a new hidden field with the value of the dropped option - for the demo it’s not hidden so you can see what’s going on.
/**
* add_to_saved()
* Adds items from the original list to the saved list
**/
function add_to_saved(e) {
// remove the old option from the original list
$(’#o’+e.value).remove();
o = ‘<option class="list2" id="o’+e.value+’" value="’+e.value+’">’+e.text+’</option>’;
// append new option to saved list
$(’#savedlist’).append(o);
// as select lists only submit what is selected and we want to submit
// the whole list, for each entry added, create a hidden field for it
// this format will be saved in a PHP array within the $_REQUEST array
// NOTE: for demo I these are plain text fields
$(’#hidden’).append(’<input type="text" name="entry[]" value="’+e.value+’" size="2"/>’);
}
And that’s it. Hopefully this might help you out, or you can improve it. Still lots to do on it, not least being to get it working on IE, but that’s for another day.
Posted in Development | No Comments »