JqueryUI draggable/droppable - Identify the drop target when the drop is invalid - jquery-ui

I am using the draggable/droppable plugins of jquery-ui and i have the following scenario.
Three droppable targets (each one accepts a single type of elements, based on class) and multiple elements (one of the three types that are accepted by the droppables).
I am using the revert option on the draggables and the accept on the droppables.
Everything works normally in terms of functionality.
What I need: to identify the drop target when the drop is invalid (dropped on a droppable that does not accept the dragged element)
What is the actual problem: All events of droppable fire only when the dragged element is acceptable by it.
I need that information so that i can display error messages customized for the specific wrong drop-target.
I really cannot wrap my head around this problem (besides using another drag/drop system..), so any pointers/ideas are welcome..

Setting up revert functions may be a good start,
Working Example
$(".draggable1").draggable({
revert: function (droppableObj) {
//if false then no socket object drop occurred.
if (droppableObj === false) {
//revert the selector object by returning true
alert('Not Dropped');
return true;
} else {
//droppableObj was returned,
//we can perform additional checks here if we like
//return false so that the selector object does not revert
return false;
}
}
});
For more info see:
https://stackoverflow.com/a/3418306/1947286
http://www.agilepro.com/blog/2009/12/while-this-functionality-is-built-into.html

Related

Handling moving of item across lists in angular-ui sortable?

I am using angular-ui sortable version 1.2
I want to handle the move of an item from one list to another, and update the back-end accordingly.
jquery-ui-sortable defines a bunch of events, including the receive event
From within that event handler, I cannot find a way to access the angular model item which was moved, and its new parent list.
See this codepen sample.
You can see that I can access the item via the scope() in the update event, but not in the receive event.
Any suggestions for a way to handle these moves? either via the receive event or otherwise?
Reorder the items in one list
UI sortable behaves intuitive if you have one list of items and just want to reorder the list. In this case you do the following if you have an array of objects in your controller like this:
$scope.yourObjects = [
{title:'Alabama'}, {title:'Ohio'}, {title:'Colorado'}
];
in your html you may create a list of these items by using ng-repeat:
<ul ui-sortable="sortableOptionsA" class="list items-container" ng-model="yourObjects">
<li class="item sortable" ng-repeat="item in yourObjects">{{item.title}}</li>
</ul>
where sortableOptions is:
$scope.sortableOptionsA = {
stop : function(e, ui) {
var item = ui.item.scope().item;
var fromIndex = ui.item.sortable.index;
var toIndex = ui.item.sortable.dropindex;
console.log('moved', item, fromIndex, toIndex);
}
};
As you can see, in the stop function we have access to all relevant information we need to be informed about the movement.
Connect 2 list of items
Now the problem get's a little bit complicated. UI Sortable gives us no information about the drop-targets that we can use directly in any way. If we move one item from one list to another list the following events are fired:
start: We have access to the item that will be moved including the scope of this item.
update: We have access to the item that is moved including the scope of this item.
Now the item is deleted from it's source list
removed: The item was removed from the source list. The scope is no longer valid (e.g. undefined).
received: The item is about to be dropped in the second list. scope is still undefined, we have only access to the sender e.g. the drag source.
Now the item is inserted in the target list.
update: The item is dropped at the target list. but we have no access to the item scope nor does there a target object exist in the event objects. The jQuery UI Sortable did not provide these information and the angular wrapper did not expose the target model in any way :(
stop: If all steps of the drag'n'drop process are done, the stop event is fired. But we have also no access to the items target scope or the target list.
What can we do if we want to get informed about a movement and which item was moved to what kind of list?
The item that was moved is accessible by ui.item.sortable.moved in the stop event. This is our item that was moved.
Which list is the drop-target can be determined by Angular's $watch function. We just listen to changes to the lists and know, which list was modified. One caveat: the source and the target list are changing, but the target list is changed at last (see the above event order). If we listen to the changes in this way:
$scope.dropTarget = null;
$scope.$watchCollection('lists[0].items', function() {
console.log('watch 0');
$scope.dropTarget = $scope.lists[0];
});
$scope.$watchCollection('lists[1].items', function() {
console.log('watch 1');
$scope.dropTarget = $scope.lists[1];
});
we have all information to get to know wich item was moved to what kind of list and what are the from and the to index:
stop:function(e, ui){
var item = ui.item.sortable.moved;
var fromIndex = ui.item.sortable.index;
var toIndex = ui.item.sortable.dropindex;
console.log(item, fromIndex, toIndex, $scope.dropTarget);
},
PLUNKR with a lot of debug code that shows what kind of information is available during the drag'n'drop process.
Remark: if you move one item from the 'Connected lists' to 'One sortable list' the log output is wrong - because there is no listener to the 'One sortable list' list!

Drag and drop in AngularJS

I am trying to implement Drag and Drop using c0deformer's jQuery UI implementation (see here: http://codef0rmer.github.io/angular-dragdrop/#/) The dragging part works fine, but I can't seem to get the functionality I am after in terms of the drop. In this application, I want to be able to drop the draggable items anywhere within a target div, i.e., I don't want the destination scope to be limited to a list-type structure (or a set of repeated divs). Mainly this is because the user will be dragging items on the fly and there will be no way of knowing how many items the user will drag and drop in advance.
I have scoured the web and cannot find an example in Angular that uses drag and drop without effectively dragging from one list to another list. Can this be done? If so, I am not sure how I would appropriately update the scope after an item has been dragged. In this example code below, the dropped items are pushed into the scope of a second list and the new scope is applied. Ideally, the scope of the dropped items is the target div I mentioned above. I'm really new to Angular, so any advice is immensely appreciated.
Snippet from c0deformer:
app.directive('droppable', function($compile) {
return {
restrict: 'A',
link: function(scope,element,attrs){
//This makes an element Droppable
element.droppable({
drop:function(event,ui) {
var dragIndex = angular.element(ui.draggable).data('index'),
dragEl = angular.element(ui.draggable).parent(),
dropEl = angular.element(this);
console.log(dropEl);
if (dragEl.hasClass('list1') && !dropEl.hasClass('list1') && reject !== true) {
scope.list2.push(scope.list1[dragIndex]);
scope.list1.splice(dragIndex, 1);
} else if (dragEl.hasClass('list2') && !dropEl.hasClass('list2') && reject !== true) {
scope.list1.push(scope.list2[dragIndex]);
scope.list2.splice(dragIndex, 1);
}
scope.$apply();
}
});
}
};
});
I recently created an angular directive for drag and drop that doesn't rely on jquery-ui. It uses the html5 drag and drop api. It also doesn't have any requirements on the format of the data to be dragged or dropped, it simply sets up a hook for you to be notified when one element is dragged onto another.
Post here: http://jasonturim.wordpress.com/2013/09/01/angularjs-drag-and-drop/
Demo here: http://logicbomb.github.io/ng-directives/drag-drop.html

JQM - Inject dynamic content at load time only

I'm trying to dynamically populate a select tag at load time (latest jQM version) using a custom template filling function.
If the fn is called in the "pagebeforechange" event, the select tag is properly initialized. Since this event is called on every page transition, I thought of moving the fn to the 'pageinit' event. This does not work, presumably because the DOM is not yet fully available. How can I coerce jQM to inject content in a page only once? Currently, I am using a kludge. There surely must be a smarter way. Thanks for any suggestions.
$(document).bind('pageinit', function () {
InitSelTagTest("#selActTag", "tplTag"); // Does not work.
});
$(document).bind("pagebeforechange", function (e, data) {
if ($("#selActTag").children().size() === 0) {
InitSelTagTest("#selActTag", "tplTag"); // Kludge, but it works
}
});
function InitSelTagTest(el,tpl) { // Append all tags to element el
var lstAllTags = JSON.parse($("#hidTag").val()); // Create tag array
// Retrieve html content from template.
var cbeg = "//<![" + "CDATA[", cend = "//]" + "]>";
var rslt = tmpl(tpl, { ddd: lstAllTags }).replace(cbeg, ").replace(cend,");
$(el).html(rslt).trigger("create"); // Add to DOM.
}
EDIT
In response to Shenaniganz' comment, it seems that the "pagebeforecreate" event could do the trick ie.
$("#pgAct").live("pagebeforecreate", function () {
// Populate tag select. Works. Traversed only once.
InitSelTag("#selActTag", "tplTag");
});
I'm not sure I fully understand your question but I'll throw a few things out there and you let me know if I can extend further.
To make something trigger only once on page load you can try to implement a regular JQuery $(document).ready(function(){}) aka $(function(){}) for the exact reason why JQuery Mobile users are told not to use it. It triggers only once on DOM load. Further pages don't trigger it because they're being switched via Ajax.
Other than that, on regular dynamic content loading you take a look at the following example I put together for someone else earlier:
http://jsbin.com/ozejif/1/edit

jQuery Draggable + Sortable - How to reject a drop into the sort?

I have a sortable list of videos and a draggable set of videos. Basically I want to make sure that the videos dragged in are not in the first 5 minutes of video. As the video lengths vary I want to test this on the drop - add up the time up to then and if not 5mins revert and show an error.
I have tried hooking into all of the callbacks for draggable and sortable (including the undocumented revert callback) to do my test but whatever I try, the dom always gets changed (and sortable calls its update callback)...
Does anyone have any suggestions?
You can revert the drag operation by calling the cancel method of the draggable widget (that method is undocumented, but its name does not start with an underscore, which arguably makes it "safer" to use reliably). It only works during the start event, though, as other events occur too late to trigger the revert animation.
However, the sortable widget will still register a drop even if the drag operation is canceled, so you also have to remove the newly-added item (during the stop event, as the start event occurs too early):
$("#yourSortable").sortable({
start: function(event, ui) {
if (!canDropThatVideo(ui.item)) {
ui.sender.draggable("cancel");
}
},
stop: function(event, ui) {
if (!canDropThatVideo(ui.item)) {
ui.item.remove();
// Show an error...
}
}
});
You can see the results in this fiddle (the fourth item will always revert).
Update: As John Kurlak rightfully points out in the comments, the item does not revert because of the call to draggable("cancel"), but because ui.sender is null in our case. Throwing anything results in the same behaviour.
Alas, all the other combinations I tried result in the item being reverted without the animation taking place, so maybe our best bet is to avoid accessing ui.sender and instead write something like:
start: function(event, ui) {
if (!canDropThatVideo(ui.item)) {
throw "cancel";
}
}
The uncaught exception will still appear in the console, though.
I found a different way. If you dont mind not having the animation of it floating back to it's original place you can always use this
.droppable({
drop: function (event, ui) {
var canDrop = false;
//if you need more calculations for
//validation, like me, put them here
if (/*your validation here*/)
canDrop = true;
if (!canDrop) {
ui.draggable.remove();
}
else{
//you can put whatever else you need here
//in case you needed the drop anyway
}
}
}).sortable({
//your choice of sortable options
});
i used this because i needed the drop event either way.

jQueryUI multiple selection by default and have lasso toggle selected status

I'm trying to implement a comparison chart-like table, and a large list of selectable objects would work just perfectly, save for a few functionality changes that I need. I see that both of these have been addressed in previous questions, but neither of them provide complete solutions.
This question addressed the multiple select behavior by default, but only says 'I did it on my own' without providing anything. Looking at the internals of selectable, I see that if I play with the !event.metaKey condition I could probably get the behavior I'm looking for without too much trouble, but was wondering if anyone had a solution that didn't involve editing the internals.
Similarly, this page addresses the lasso toggle effect that I desired, but I'm not sure where in the code the lasso functionality was changed and as the rest of the script (the sortable functionality) is reported to not work in chrome or IE8 (link) and is outdated as this point, I'd rather not rely on the entire thing.
So if anyone could help me out with either of these, I'd appreciate it. Thanks
[Edit] Formatting...
I'm sure there's a better way to do this, but here's what I've did within the selectable js file.
For the always multi-selection:
I added an option 'alwaysMulti' (default false). Then I replaced the three instances of !event.metaKey with (!event.metaKey && !options.alwaysMulti) and the two instances of event.metaKey with (event.metaKey || options.alwaysMulti).
To get the selection lasso to toggle the selected status, I found the changes I needed from the second page I linked to. I also added an option 'lassoToggle' (default false) to trigger this functionality. Within _mouseDrag, there is a condition if (hit), it gets changed to the following:
if (hit) {
// SELECT
selectee.deselect = false;
if (selectee.selected || (options.lassoToggle && (selectee.startselected && event.metaKey)) {
selectee.$element.removeClass('ui-selected');
selectee.selected = false;
selectee.deselect = true;
}
if (selectee.unselecting) {
selectee.$element.removeClass('ui-unselecting');
selectee.unselecting = false;
}
if (!selectee.selecting && (!options.lassoToggle || !selectee.deselect) {
selectee.$element.addClass('ui-selecting');
selectee.selecting = true;
// selectable SELECTING callback
self._trigger("selecting", event, {
selecting: selectee.element
});
}
if(selectee.deselect && options.lassoToggle) {
selectee.$element.removeClass('ui-selecting');
selectee.selecting = false;
selectee.$element.addClass('ui-unselecting');
selectee.unselecting = true;
// selectable UNSELECTING callback
self._trigger("unselecting", event, {
unselecting: selectee.element
});
}
}
Note: The event.metaKey change for the multi-select isn't in that code sample.
Hopefully this helps someone else!

Resources