First off the plugin makes use of jQuery's clone() function to move elements out of an html tree (html tree consisting of ol/ul's and li's) and into separate div's for the various finder windows. I don't mind the using of clone(), but it does screw up any processing that wants to manipulate the elements that were cloned by id. Basically it clones the nodes, so you'll have 2 nodes in the DOM with the same id, and you won't be able to find both with jQuery (it will return the first it encounters). To get around this you can use classes instead of id's but really in the end there should only be one copy of the elements in the tree, and cloning to me seemed like a hack.
To fix it I first started storing as part of finder viewer div the original parent id (old_parent_id) of where the given tree fragment started:
Finder.prototype.selectItem = function(url,noCache,targets){
var self = this,
settings = self.settings,
target = (targets) ? targets[0] : null,
listItem = (targets) ? targets[1] : null,
type = (listItem) ? listItem[0].className.match(/(file|folder)/)[0] : 'folder',
// data will be whatever is found under the current element (listItem)
data = (url == 'root')
? (settings.url) ? null : this.element
: $('> ul, > ol, > div',listItem).eq(0),//.clone(true),
url = (url == 'root' && typeof settings.url === 'string') ? settings.url : url;
if(listItem != null) {
var oldId = listItem.attr("id");
} else {
if(oldId == null || oldId == 'undefined') {
oldId = "";
}
}
......
// Append the new data
self.appendNewColumn(url,data,[target,listItem],type,oldId);
......
Finder.prototype.appendNewColumn = function(url,data,targets,type,oldId){
.......
// Specify new column, and add necessary attributes
newColumn = $('
Then I made sure that in contrast to before these changes when we were just removing the cloned items, we now are careful about putting the cloned items back where they started.
Finder.prototype.select = function(target,noCache,actionType) {
......
// Remove visible lists which should not be visible anymore
wrapperLists.each(function(){
var finderListWrapper = $(this),
finderListLevel = finderListWrapper.attr('data-finder-list-level');
if( finderListLevel >= targetLevel ) {
$('.ui-finder-list-item.ui-finder-list-item-active',finderListWrapper)
.removeClass('ui-finder-list-item-active ' + classesActive ); }
if( finderListLevel > targetLevel ) {
debug(finderListWrapper.attr("old_parent_id"));
if(finderListWrapper.attr("old_parent_id") != null && finderListWrapper.attr("old_parent_id") != 'undefined' && finderListWrapper.attr("old_parent_id").length > 0) {
debug(finderListWrapper.children().find("ul"));
var old_parent = $("#"+finderListWrapper.attr("old_parent_id"));
debug("old_parent: "+old_parent);
finderListWrapper.find("div.ui-finder-content").children().each(function() {
try {
var child = $(this);
// prepend is important b/c the finder code looks for the first ul/li/div when rendering
// and therefore this stuff needs to go back to the top of the old_parent's children
old_parent.prepend(child);
} catch(e) {
debug(e);
}
});
}
finderListWrapper.remove();
}
});
and presto we are no longer cloning the nodes and we have the same behaviour as when we did clone(). Note that this code is not thoroughly tested, but once it is I'll submit it back to the jQuery tree guy to see if he wants to incorporate the changes. Also note, that I tried to indicate where everything goes by using function names and "...." to represent, skips, I couldn't really give line numbers as they have all changed in my version.
So now that I had the cloning stuff straightened out I needed to implement drag and drop on the finder so I could drag things from one part of the finder tree representation to another. To do that I setup my ul's to be sortables in the finder code after a new column was added (in order to ensure that all columns are always sortable):
Finder.prototype.appendNewColumn = function(url,data,targets,type,oldId){
.....
//Append new column
// Plain DOM scripting used as opposed to jQuery as it's faster
self.wrapper[0].appendChild(newColumn[0]);
newColumn[0].appendChild($(data)[0]);
$(newColumn[0]).find(".child_container").sortable({
connectWith: '.child_container',
appendTo: $(".ui-finder-container"),
opacity: .5,
zIndex: 100,
helper: 'clone',
revert: true,
scrollElements: '.ui-finder-content',
receive: function(event, ui) {
debug(ui.sender);
debug($(this));
debug(ui.item);
var sender_id = ($(this)).attr("id").split("_")[2];
var other_id = $(ui.item).attr("id").split("_")[1];
debug(sender_id+" "+other_id);
if(sender_id != other_id) {
if(ui.sender.attr("id").indexOf("child_container_") > -1) {
$(ui.sender).find(ui.item.attr("id")).remove();
} else if(ui.sender.attr("id").indexOf("li") > -1){
ui.item.remove();
}
} else {
debug("attempting to cancel");
$(ui.sender).sortable('cancel');
display_message("You dragged a parent onto a child, which would remove the parent and child from the interaction entirely");
}
}
});
$(newColumn[0]).disableSelection()
This snippet sets up the child container to be sortable, and sets up some of the options for sortables. I need to work on making this an option in the finder code, as this is quite specific to what I am doing... Key options are the helper:'clone', which must be this way to drag across the finder viewer div's (if helper is not clone the element can only be dragged within its parent div). Also appendTo: $(".ui-finder-container"), is important, because it tells jQuery to append the cloned helper to the upper container that contains all the finder viewer div's, again this is needed so we can drag across the finder viewer areas. Finally connectWith: '.child_container' tells jQuery that we can drop the element on any other sortable list with a class of 'child_container', which all my ul's are classed as in my finder viewer areas.
The last piece to the puzzle is the receive function which calls back anytime a sortable element is dropped on another sortable element. In here I do some work to figure out if the place where the element that is dropped is a child of the original element, and I then revert the change if so. I do this because I don't want to allow an element to be dropped onto itself basically.
The next thing I had to do to get this going was hack up the jQuery sortables code to use more than one element as a drop zone and scroll all the dropzones. I changed the _mouseDrag method to use the connectWith selector to find all the dropzones and loop through them to scroll any that are appropriate:
_determineScroll:function(event, scrollParent, o) {
if(scrollParent[0] != document && scrollParent[0].tagName != 'HTML') {
if((this.overflowOffset.top + scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity)
scrollParent[0].scrollTop = scrolled = scrollParent[0].scrollTop + o.scrollSpeed;
else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity)
scrollParent[0].scrollTop = scrolled = scrollParent[0].scrollTop - o.scrollSpeed;
if((this.overflowOffset.left + scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity)
scrollParent[0].scrollLeft = scrolled = scrollParent[0].scrollLeft + o.scrollSpeed;
else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity)
scrollParent[0].scrollLeft = scrolled = scrollParent[0].scrollLeft - o.scrollSpeed;
} else {
if(event.pageY - $(document).scrollTop() < o.scrollSensitivity)
scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity)
scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity)
scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity)
scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
}
},
_mouseDrag: function(event) {
//Compute the helpers position
this.position = this._generatePosition(event);
this.positionAbs = this._convertPositionTo("absolute");
if (!this.lastPositionAbs) {
this.lastPositionAbs = this.positionAbs;
}
//Do scrolling
if(this.options.scroll) {
var o = this.options, scrolled = false;
var currentElements = [];
if(this.options.scrollElements != null) {
currentElements = $(this.options.connectWith);
}
for(var j = 0; j < currentElements.length; j++) {
var scrollParent = $(currentElements[j]).scrollParent();
this._determineScroll(event, scrollParent, o);
}
// and do the the helper's scroll parent (default)
this._determineScroll(event, this.scrollParent, o);
if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour)
$.ui.ddmanager.prepareOffsets(this, event);
}
//Regenerate the absolute position used for position checks
this.positionAbs = this._convertPositionTo("absolute");
//Set the helper position
if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px';
if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px';
//Rearrange
for (var i = this.items.length - 1; i >= 0; i--) {
//Cache variables and intersection, continue if no intersection
var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item);
if (!intersection) continue;
if(itemElement != this.currentItem[0] //cannot intersect with itself
&& this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before
&& !$.ui.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked
&& (this.options.type == 'semi-dynamic' ? !$.ui.contains(this.element[0], itemElement) : true)
) {
this.direction = intersection == 1 ? "down" : "up";
if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) {
this._rearrange(event, item);
} else {
break;
}
this._trigger("change", event, this._uiHash());
break;
}
}
//Post events to containers
this._contactContainers(event);
//Interconnect with droppables
if($.ui.ddmanager) $.ui.ddmanager.drag(this, event);
//Call callbacks
this._trigger('sort', event, this._uiHash());
this.lastPositionAbs = this.positionAbs;
return false;
},
I am going to try and submit this to jQuery as a enhancement to the current scrollables functionality, as I can see this being useful for a lot of folks (in terms of scrolling multiple drop zones).
So that is about it, I now have a working, non-cloning finder tree viewer along with drag and drop working between the finder viewer areas, and even some rudimentary validation (don't drop a element on its own sub-tree). I should probably spend the time to package this up and give it back to jQuery but I just don't have that much time. If anyone is interested in doing this, I'll be glad to help.
Ping me with questions/complaints in the comments...
where is the demo?
ReplyDelete