1

NOTE: Exact description of question follows CSS below. Sample code can be seen in this fiddle.

I have a parent div with a list of child divs within it, that looks like the following:

List of children divs

HTML for said container and children is:

<div class="categories_container">
    <div class="category one">One</div>
    <div class="category two">Two</div>
    <div class="category three">Three</div>
    <div class="category four">Four</div>
    <div class="category five">Five</div>
    <div class="category six">Six</div>
</div>

Where the classes .one, .two, .three, etc... are their relative position in the list.

The children elements are positioned with absolute positioning, within their parent.

CSS as follows (some properties not shown for simplicity):

.categories_container {
    height: 324px;
    width: 100%;
    position: relative;
}
.category {
    height: 50px;
    width: 98%;
    position: absolute;
    left: 0px;
    z-index: 0;
}
.one {
    top: 0px;
}
.two {
    top: 54px;
}
.three {
    top: 108px;
}
.four {
    top: 162px;
}
.five {
    top: 216px;
}
.six {
    top: 270px;
}

As can be seen in this fiddle, you can click (and hold) on any one of the child elements and move it up and down within the parent div. When you release the mouse, the selected child snaps back to its original position.

Question:

How can I detect if the selected element has been dragged overtop of another? I don't only want to know if they are overlapping, but would like to put a range on it. Something like...

if(center of current child is overtop a set range within another child){
    do stuff...
}

What I'd like to do for now (as a proof of concept) is to have the underneath child's background color change WHILE the vertical center of the selected child is within the range 0.4-0.6 of the bottom child's height. If the selected child is dragged out of said region, the background should change back.

I've tried something like:

$('.category').mouseover(function(){
    if(dragging){
        ... execute code...
    }
});

But it seems that if I am dragging one element over the other, the bottom element cannot see the mouse, and so the function is never executed.

Also:

I've tried a few different methods to keep the cursor as a pointer while dragging, but no matter what it switches to the text cursor whilst dragging. So any help with that would also be appreciated.

For the pointer thing I've tried putting $(this).css('cursor', 'pointer'); in the mousedown and mouse move functions, but to no avail.

Thanks in advance! Sorry if any of this is confusing.

Birrel
  • 4,754
  • 6
  • 38
  • 74
  • 1
    jQueryUI sortables does this sort of thing for you... – dandavis Feb 24 '14 at 03:56
  • So it does. Never heard of it before. Just looking at [this](https://jqueryui.com/sortable/) link, do you know quickly off the top of your head whether or not it can be written so that the elements do not have any horizontal freedom? I'd prefer them to only move vertically, as in my example. – Birrel Feb 24 '14 at 04:17
  • yes: http://api.jqueryui.com/sortable/#option-axis – dandavis Feb 24 '14 at 06:58
  • I've been playing around with my own code and have come up with ***[this](http://jsfiddle.net/54QM2/15/)*** so far. There are a couple things that need to be worked out, like right now you have to know before-hand how many list items there will be (should be solvable via JS), and the list order gets all messed up if you go too fast. Couple little bugs to work out, but I'd prefer to go this route. Thanks though! If I can't get it just right, I may be going with your solution! – Birrel Feb 24 '14 at 07:50
  • **[HERE](http://jsfiddle.net/gGB4x/9/)** is a nice solution I've come up with. Works well. – Birrel Mar 02 '14 at 00:37

1 Answers1

2

Here is the solution I came up with, purely with JS and JQuery, with no external libraries required and without using JQueryUI Sortables.

HTML:

<div class="list_container">
    <div class="list_item">One</div>
    <div class="list_item">Two</div>
    <div class="list_item">Three</div>
    <div class="list_item">Four</div>
    <div class="list_item">Five</div>
    <div class="list_item">Six</div>
</div>

where list_container holds the individual list_item elements. Is it the latter of the two which can be moved around to create your sorted list. You can put just about anything you'd like within list_item and it'll still work just fine.

CSS:

.list_container {
    position: relative;
}
.list_item {
    position: absolute;
    z-index: 0;
    left: 0px;
}
.list_item.selected {
    z-index: 1000;
}

Please visit this fiddle for the full list of CSS rules (only necessary ones are shown above).

JavaScript:

I'll go through this bit-by-bit and then show the full code at the bottom.

First off, I defined an array that matches up index numbers with their written counterparts

var classes = new Array("one", "two", "three", ...);

This is used to create classes dynamically (upon page load). These classes are used to order the list. You are only required to populate this array with as many items as you will have in your list. This is the one downfall of the code I have written and am unsure of how to overcome this issue (would be VERY tedious to enter in the elements for a list of hundreds of items, or more!)

Next, a few other variables:

var margin = 2;       // Space desired between each list item
var $el;              // Used to hold the ID of the element that has been selected
var oldPos = 0;       // The position of the selected element BEFORE animation
var newPos = 0;       // The position of the selected element AFTER animation (also current position)
var dragging = false; // Whether or not an item is being moved

var numElements = $('.list_container > div').length;

// selectionHeight is the height of each list element (assuming all the same height)
// It includes the div height, the height of top and bottom borders, and the desired margin

var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;

var classInfo = '';  // classInfo will be populated with the information that is used to dynamically create classes upon page load

When page loads, go through each list_item and assign it a class according to its initial position in the list. Also add to classInfo the location of the TOP of the list item.

$('.list_container .list_item').each(function (index) {
    $(this).addClass(classes[index]);
    classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});

Now, using classInfo that was created above, dynamically write the classes to the page.

var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);

The above bit of code will write the required classes into the HTML of the page. If you view the source of the page, you can see the classes in the head of the page.

Now for the ordering part. First, mousedown

$('.list_item').mousedown(function (ev) {
    $el = $(this);
    oldPos = $el.index() + 1;
    newPos = oldPos;
    dragging = true;
    startY = ev.clientY;               // Gets the current mouse position
    startT = parseInt($el.css('top')); // Gets the current position of the TOP of the item
    $el.addClass('selected');          // Adding class brings it to top (z-index) and changes color of list item
});

Next, the mousemove and mouseup functions are tied together

$(window).mousemove(function (ev) {  // Use $(window) so mouse can leave parent div and still work
    if (dragging) {
        $el.attr('class', 'list_item')  // Remove the numbered class (.one, .two, etc)
        $el.addClass('selected');       // Add this class back for aesthetics

        // ----- calculate new top
        var newTop = startT + (ev.clientY - startY);
        $el.css('cursor', 'pointer');
        // ------

        //------ stay in parent
        var maxTop = $el.parent().height() - $el.height();
        newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
        $el.css('top', newTop);
        //------

        newPos = getPos(newTop, selectionHeight); // Determine what the current position of the selected list item is

        // If the position of the list item has changed, move the position's current element out of the way and reassign oldPos to newPos
        if (oldPos != newPos) {
            moveThings(oldPos, newPos, selectionHeight);
            oldPos = newPos;
        }
    }
}).mouseup(function () {
    dragging = false;            // User is no longer dragging
    $el.removeClass('selected'); // Element is no longer selected
    setNewClass($el, newPos);    // Set the new class of the moved list item
    $el.css('top', (newPos - 1) * selectionHeight);  // Position the moved element where it belongs. Otherwise it'll come to rest where you release it, not in its correct position.
});

Finally, the three functions getPos, moveThings and setNewClass are as follows:

function getPos(a, b) { // a == newTop, b == selectionHeight
return Math.round( (a/b) + 1 ); 
}

getPos works by finding out which region the selected element is currently in. If newTop is less than .5b, then it is in region 1. If between .5b and 1.5b, then it is region 2. If between 1.5b and 2.5b, then in region 3. And so on. Write out a few cases on a piece of paper and it'll make sense what is happening.

function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
    var first = classes[b - 1];  // What is the current class of the item that will be moved
    var $newEl = $('.list_container .' + first);  // ID of element that will be moved

    if (a < b) { // oldPos less than newPos
        var second = classes[b - 2]; // The new class of the moved element will be one less
        var newTop = parseInt($newEl.css('top')) - c; // Top of element will move up
    } else { // oldPos more than newPos
        var second = classes[b]; // The new class of the moved element will be one more
        var newTop = parseInt($newEl.css('top')) + c; // Top of element will move down
    }

    // The following line of code is required, otherwise the following animation 
    // will animate of from top=0px to the new position (opposed to from top=currentPosition)
    // Try taking it out and seeing
    $newEl.css('top', parseInt($newEl.css('top')));
    $newEl.removeClass(first); // Remove the current numbered class of element to move
    // Move element and remove the added style tags (or future animations will get buggy)
    $newEl.animate({top: newTop}, 300, function () {
        $newEl.removeAttr('style');
    });
    $newEl.addClass(second); // Add the new numbered class

    return false; // Cleans up animations
}

The function above is what does the actual animation part and moves the list items around to accommodate the selected list item.

function setNewClass(e, a) { // e == selected element, a == newPos
    // Remove 'selected' class, then add back the 'list_item' class and the new numbered class
    e.attr('class', 'list_item').addClass(classes[a-1]);
}

** All JavaScript together: **

var classes = new Array("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeem", "eighteen", "nineteen", "twenty", "twentyone", "twentytwo", "twentythree", "twentyfour");

$(document).ready(function () {
    var margin = 2;
    var $el;
    var oldPos = 0;
    var newPos = 0;
    var dragging = false;

    var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;

    var classInfo = '';

    $('.list_container .list_item').each(function (index) {
        $(this).addClass(classes[index]);
        classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
    });

    var style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = classInfo;
    document.getElementsByTagName('head')[0].appendChild(style);

    $('.list_item').mousedown(function (ev) {
        $el = $(this);
        oldPos = $el.index() + 1;
        newPos = oldPos;
        dragging = true;
        startY = ev.clientY;
        startT = parseInt($el.css('top'));
        $el.addClass('selected');
    });

    $(window).mousemove(function (ev) {
        if (dragging) {
            $el.attr('class', 'list_item')
            $el.addClass('selected');

            // ----- calculate new top
            var newTop = startT + (ev.clientY - startY);
            $el.css('cursor', 'pointer');
            // ------

            //------ stay in parent
            var maxTop = $el.parent().height() - $el.height();
            newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
            $el.css('top', newTop);
            //------

            newPos = getPos(newTop, selectionHeight);

            if (oldPos != newPos) {
                moveThings(oldPos, newPos, selectionHeight);
                oldPos = newPos;
            }
        }
    }).mouseup(function () {
        dragging = false;
        $el.removeClass('selected');
        setNewClass($el, newPos);
        $el.css('top', (newPos - 1) * selectionHeight);
    });
});

function getPos(a, b) { // a == topPos, b == selectionHeight
    return Math.round((a / b) + 1);
}

function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
    var first = classes[b - 1];
    var $newEl = $('.list_container .' + first);

    if (a < b) { // oldPos less than newPos
        var second = classes[b - 2];
        var newTop = parseInt($newEl.css('top')) - c;
    } else { // oldPos more than newPos
        var second = classes[b];
        var newTop = parseInt($newEl.css('top')) + c;
    }

    $newEl.css('top', parseInt($newEl.css('top')));
    $newEl.removeClass(first);
    $newEl.animate({
        top: newTop
    }, 300, function () {
        $newEl.removeAttr('style');
    });
    $newEl.addClass(second);

    return false; // Cleans up animations
}

function setNewClass(e, a) { // e == selected element, a == newPos
    e.attr('class', 'list_item').addClass(classes[a - 1]);
}
Birrel
  • 4,754
  • 6
  • 38
  • 74
  • **EDIT** [THIS](http://jsfiddle.net/gGB4x/9/) fiddle has a solution which doesn't have the classes array and will work for any length of list. using the `toNumber()` function that I wrote. It doesn't truly write the number (i.e. 22 is twotwo), but each class is unique and that is all that is required. – Birrel Mar 02 '14 at 00:35
  • 1
    **EDIT #2**... Last time I add to this answer... **[THIS](http://jsfiddle.net/gGB4x/14/)** solution fixes an animation problem, and works *perfectly* (as far as I am concerned)! A couple changes: 1) Margin is changed in CSS now, 2) Parent div height is set automatically, 3) added `.stop()` to animation and now things work better, 4) Changed name of `toNumer()` class from previous comment to `numToClass()` to avoid any confusion. – Birrel Mar 02 '14 at 06:11
  • 1
    nice solution and write up! – dandavis Mar 02 '14 at 22:42