I have a sortable list (of tasks). It helps prioritize tasks, so I want to keep this functionality. Now I want to add subtasks to the functionality: I want to enable users to drag one task over to another task and drop it there to turn it into a subtask.
Applying .draggable() and .droppable() to items that are already sortable has no effect. What could I do?
I put together a demo of how to do this... but it may not be the best method. Here are some problems I've discovered:
Because this code uses the placeholder to figure out where you are moving the list, you can only drop an item inside another item if you approach it from the top. I did get a working version where you could drop an item anywhere inside the base item, but the code was just too messy and cumbersome.
Sometimes when an item from the other list is dropped in an item, it becomes stuck. I'm not sure why, but it becomes unstuck when the list group is moved to the other list.
I'm sure there is a better method, one that calculates the intersection of list items (just like the sortable script does). But this is a quick and dirty method.
$(function() {
var phTop, container, indx;
$("#sortable1, #sortable2").sortable({
connectWith: '.connectedSortable',
beforeStop: function(e,ui){
phTop = ui.placeholder.position().top;
container = ui.placeholder.parent();
indx = ui.placeholder.index();
},
stop: function(e,ui){
var list = container.find('> li').eq(indx);
// 15 is a pixel tolerance between the two items (dragging in from the top)
if ( Math.abs( phTop - ui.position.top ) < 15 ) {
// prevent list depth > 1
if (ui.item.find('li').length > 0) { return; }
// add ul inside of li to make subgroup
if (!list.find('ul').length) { list.append('<ul></ul>'); }
ui.item.appendTo( list.find('> ul') );
}
container.find('li > ul:empty').remove(); // remove empty subgroups
}
}).disableSelection();
});
After I posted my question I had no patience, and I decided to ignore UI.sortable altogether, building the required functionality from draggable and droppable and using special divs as spacers that would swell up on dragover to facilitate dropping in between tasks.
That worked to some degree, except it's all much more code and it's a lot more jittery and bugprone than sortable, even with the refreshPositions option set to true. Still, there might be other valid reasons to want to circumvent UI.sortable.
In very brief faux code: $(.taskitem).draggable
revert: invalid
start: animate height of spacers from 0 to 5
$(.spacer).droppable
over: animate height from 5 to 50
out: animate height back to 5
drop: insert draggable after spacer
find spacer with same index as draggable and move it along
Related
JQuery UI Sortable works great for lists with no gaps, but say I want to render a list of items with gaps, e.g.
1, 2, empty, 4, 5, 6, empty, 8
where the number represents the slot number. The behavior expected then would be if the user drags an element over the 2 slot, the 2 value gets pushed to the empty slot 3, and the user could drop the new element in the 2 slot, whereas if they drag a new element over the empty 3 slot, the list items would not push down, and the user could just drop the new item into the empty 3 slot. Hopefully this makes sense.
I've been looking at the JQuery UI Sortable code, and it seems that I need to utilize the change and receive callbacks in order to achieve this, but, being new to JQuery/JS in general, it's not clear to me what to use to add these empty slot placeholders and manage the selection list so that I don't break the sorting functionality with custom code.
Any pointers, examples, etc. would be much appreciated.
After banging my head on this for a while, I created a jsFiddle: http://jsfiddle.net/pdHnX/
To help explain the problem. I believe everything I'm trying to accomplish can happen in an overridden _rearrange method. The fiddle code handles the case where an item is replacing a filler item, but there is an odd issue, where if you drag an item from the item list to the filler list, drop the item, then drag the same item within the filler list, the filler list is shrunk by 1, which is a problem.
There are more issues once you start dragging more items into the filler list, but this is where I'm at with the problem at the moment.
I'm not sure if I understood you correctly, but here's what I came up with:
http://jsfiddle.net/py3DE/
$(".source .item").draggable({ revert: "invalid", appendTo: 'body', helper: 'clone',
start: function(ev, ui){ ui.helper.width($(this).width()); } // ensure helper width
});
$(".target .empty").droppable({ tolerance: 'pointer', hoverClass: 'highlight',
drop: function(ev, ui){
var item = ui.draggable;
if (!ui.draggable.closest('.empty').length) item = item.clone().draggable();// if item was dragged from the source list - clone it
this.innerHTML = ''; // clean the placeholder
item.css({ top: 0, left: 0 }).appendTo(this); // append item to placeholder
}
});
Assumptions:
If you drag item from the top list - it will be "cloned" (original will remain in the list)
If you drag item from the bottom list - it will move (leaving empty placeholder)
If you drop any item on a bottom list's placeholder - it will replace the placeholder
If you drop any item on bottom list's non-empty placeholder/item - it will replace it
sizes of both lists always stay the same
Let me know if this helps or are you looking for something else.
Also explaining the whole task (what's the purpose of this dragging) might help :-)
Sorry, this was a really difficult question to detail out. The requirements were really specific and hard to describe.
Here is the solution I was looking for.
The others answers here, while they may have been solutions to the poorly described problem, were not what I was looking for.
http://jsfiddle.net/vfezJ/
Unfortunately, the sortable.js code is very hairy so the code linked there is also very hairy. The solution I've posted also makes some assumptions on CSS classes and DOM ids to get the job done.
http://jsbin.com/igozat/2
Here's something I whipped up. It's not perfect or deserving of much credit because sortable() can't be interruped mid-drag. If that was possible, then a drop action could take over completely. Also, my attempt to re-sort was buggy. I know that this methods works with just regular jQuery, but it didn't work with jQuery UI mucking me up.
At risk of sounding like a jerk, if it were me, I'd write the functionality with just jQuery. jQuery UI isn't all its cracked up to be. :P
When I drag the draggable, the element containing the droppables keeps getting triggered (because it's also a droppable), even though it's behind the droppables and they're stacked right on each other. It's behaving like there's a gap between the droppables, and there isn't one.
I've made an example on jsFiddle and here's the screenshot of the offending behavior.
If I add padding to .parent (for example padding: 0.2em 0.2em 0em 0.2em the behavior is even worse.
First of all, I hope you found out a solution in these months, I'm answering this because I'm working with jQueryUI in this period and I thought it would be a good exercise to try to find an answer. Also, I hate unanswered questions ^_^
Unfortunately, it really looks like the browser reacts as if between those .child elements there's some space, enough to trigger the *over events for the .parent. The only idea I came up with is to try to detect if, when the dropover events triggers on the parent, the mouse position is actually inside a child element. If so, you should give the accepting_drops class to the child element instead of the parent.
Here's the code (I made a jsFiddle to show my solution in action):
HTML and CSS are unaltered, so I won't copy them again
Javascript
$('.dragme').draggable({
helper: 'clone'
});
$('.parent,.child').droppable({
greedy: true,
tolerance: 'pointer',
});
$(".parent").on("dropover", function(event, ui) {
var first = $(".child:first");
var last = $(".child:last");
if((event.originalEvent.pageX > first.offset().left) &&
(event.originalEvent.pageY > first.offset().top) &&
(event.originalEvent.pageX <= (last.offset().left + last.width())) &&
(event.originalEvent.pageY <= (last.offset().top + last.height()))) {
$(this).removeClass("accepting_drops");
}
else {
$(this).addClass("accepting_drops");
$(".child").removeClass("accepting_drops");
}
}).on("dropout", function() {
$(this).removeClass("accepting_drops");
});
$(".child").on("dropover", function() {
$(".parent").removeClass("accepting_drops");
$(".child").removeClass("accepting_drops");
$(this).addClass("accepting_drops");
}).on("dropout", function() {
$(this).removeClass("accepting_drops");
});
I removed the hoverClass: 'accepting_drops' line because we're overriding the default behaviour of the draggable component. For the parent div, if when a dropover event triggers I'm also inside a child element, I remove the accepting_drops class from it, otherwise I remove it from any child who could have it and add it to the parent instead. When a dropout event triggers on it, I remove the accepting_drops class.
For the child, the behaviour is almost standard, on a dropover event I remove the accepting_drops class from everything else and add it to the child, on a dropout event I remove it.
The behaviour is still a bit of a mistery, but this workaround should do the trick.
I'm trying to build an interface tool which essentially allows users to build a grid out of common UI elements.
Here's a jsFiddle: http://jsfiddle.net/FX4Fw/
Essentially, the idea is that you drag content elements (picture, headline, standfirst, etc) into the grey placeholder at the bottom. Once they're in there, they should no longer be Draggables (because this breaks the CSS grid system they inherit) and they can then be resized. The original items in the UI should stay where they are, so the user is essentially cloning them into the box to be positioned.
This almost works in my demo, but when the user grabs a UI element and drags it into the placeholder, I then remove the ui-draggable class from the cloned element that ends up inside the placeholder. This also removes it from the original source element (I want this to stay where it is) so it's no longer usable.
Is there a way to combine these things so they work in tandem? Hopefully it's clear what I'm trying to do.
Never mind - found the answer here: https://stackoverflow.com/a/3041887/176615
(basically this code)
stop: function(event, ui) {
//check it wasn't here previously
if(!ui.item.data('tag') && !ui.item.data('handle')) {
ui.item.data('tag', true); //tag new draggable drops
ui.item.removeClass('ui-draggable'); // dirty hack
}
},
I am working on a tool based on jQuery UI draggable functionality.
I have a number of boxes in the left column of the table. When they are dragged in yellow area, I would expect the remaining divs to move upwards to fill the space left by the box that was moved.
But it's not happening. Why?
It is pretty difficult to test but from my knowledge on the question here is a possible cause/solution to this.
The droppable plugin does not remove the dragged element from its original markup position, it is visually moved to the droppable element (with some option allowing to accept to drop certain elements or not, events, etc).
The elements have a position: relative css rule, which represents the "normal flow" for elements (in the order they appear in the markup). So even if the element is visually placed elsewhere on the page with css, its place in the markup is still the same and it is still taking the space it normally should.
This fiddle illustrate what i'm trying to explain :-)
By looking at the source code form the "working website", they actually remove the dragged element from the original draggable list and re-create it in the droppable list !
When they define the .droppable() they do this:
h.droppable({
tolerance: "intersect",
accept: ".card",
greedy: true,
drop: function (a, b) {
card = b.draggable;
putCardIntoStack(card, c)
}
});
On the drop event, they call putCardIntoStack(card, c) passing the currently dragged element as the card parameter. Within this method, they remove the original "card" (a.remove()) and re-create it in the dropzone (newcard = createCard();):
function putCardIntoStack(a, b) {
progressVal = $('#progBarRd').width();
card_idDOM = a.attr('id');
card_idDB = card_idDOM.substr(IDPREFIX_CARD.length, card_idDOM.length - IDPREFIX_CARD.length);
stack_idDB = b.substr(IDPREFIX_STACK.length, b.length - IDPREFIX_STACK.length);
$.ajax({
url: URL_DRAGDROPAJAX,
type: 'POST',
data: 'action=movecard&cardid=' + card_idDB + '&tostack=' + stack_idDB + '&prog=' + progressVal
});
// 'a' is the card
// they extract the id/content from the dragged card
cardId = a.attr('id');
cardLabel = a.text();
// they remove the card from the DOM
a.remove();
// they create a new card with the extracted info
newcard = createCard(cardId, cardLabel);
newcard.addClass('stackcard');
// and append it to the dropzone
$('#' + b).removeClass("empty").find('.stackDrop').append(newcard);
globalcheck()
}
jQuery UI does a similar thing on the droppable demo page. On the drop event, they call a function deleteImage() which removes the dragged image from the original markup and appends it to the drop zone.
I hope I'm clear enough :-)
I also hope I'm right, it is pretty difficult to test quickly but it makes sense :-)
I am using jQuery draggable/droppable to drag (divs) from one list to another. Both lists have a fixed number of list items, and the list items themselves and can either be empty or already contain a div.
I am dragging a div to a list item. If the list item already contains a div then I would like that div to be pushed down to the list item below it - so as to make way for the div I am dragging. I can do this with the "over" event in droppable. But I would like this to happen only after the user has paused there for a cetain period of time, say 0.5 second. How can I do this?
Also, if the user decides not to drop then I would like the displaced item to come back to its original position. Again, what would be the easiest of accomplishing that?
Thanks for your help
William
I just happen to be doing something similar. In my case, I am using the treeTable plugin. I want to expand a collapsed tree if it is hovered for some amount of time. Here is what I have so far. I just happen to be working on this today. Note the use of a timeout to delay the operation.
var timeout = null;
var clear = function() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
}
$(".selector").droppable({
over: function(e, ui) {
if (! $(this).is("expanded")) {
clear();
timeout = setTimeout(function() {
$(this).expand();
timeout = null;
}, 1000);
}
}
, out: clear
, drop: function(e, ui) {
clear();
...
}
});
I'm still having a problem with this. After one branch gets expanded, the rows below it get pushed down to make room for the expanded branch. I haven't dropped yet and so I go hover over the first new element in the expanded branch. The problem is that this element does not get the hoverClass, the first element that got pushed down below the new branch gets it. It is like jquery doesn't figure out that it got moved down. I imagine that you are likely to encounter this problem given what you've described.
I was on jquery-1.4.2 but then I updated to jquery-1.6.4 to see if that would fix the problem. Both versions exhibit the same behavior. Also, I tested this in Chrome 14.0.835.202, Opera 11.52, Firefox 3.6.23 and IE7.0.5730.13. It happens the same way in all of them. So, either jquery has a bug or I've done something to cause this.
About your second question. I'm not sure I completely understand what you're going after. Are you looking for the