Draggable Elements That Push Others Out Of Way

Published by Chris Coyier

Aside from a few esoteric tricks involving stuff like the resize handle on textareas, draggable elements is JavaScript territory on the web. E.g. click on element, hold down mouse button, drag mouse cursor, element drags with the mouse, release mouse button to release element. Or the touch equivalents. Fortunately this is well-tread territory. Time tested tools like jQuery UI offer Draggable (and other similar methods) to make this easy.

But recently in trying to achieve a certain effect (see title of this article) I couldn't quite get jQuery UI to do it how I wanted. But I got it done and here's how.

I was trying to recreate the effect in the sidebar in Keynote where you can drag to rearrange slides. Here's the finished effect:

You can get super close with some really basic jQuery UI configuration. We're using the Sortable method here. This is specifically for sorting lists, which is what we are doing, which is a bit like using both Draggable and Droppable.

$(".all-slides").sortable({
  
  axis: "y",
  revert: true,
  scroll: false,
  placeholder: "sortable-placeholder",
  cursor: "move"

});

On some basic HTML like this:

<div class='all-slides'>

  <div class='slide'>Slide</div>
  <div class='slide'>Slide</div>
  <div class='slide'>Slide</div>

  <!-- etc -->

The "placeholder" in the configuration is an element (with the provided class name) that gets inserted amongst the slides where that slide would drop if you released the mouse button right now. You can style that however you want with CSS, so I made it look like the blue-line-on-the-left like Keynote.

The trouble here is we don't get that "push out of the way" effect we're going for. The placeholder just shows up immediately. Originally I tried to solve it by using a @keyframes animation to expand the height of the placeholder from 0 to the slide height and then stay there with fill-mode. That works for the appearance of the placeholder, but jQuery UI just rips that placeholder out of the DOM when it goes away which didn't allow for a graceful exit.

So some deeper trickery was in order. Fortunately, after bemoaning the difficulty on Twitter, AJ Kandy found a handy example doing just what I needed.

Bear with me, this gets a bit complicated:

  1. Loop through all the slides
  2. Create a duplicate of each slide
  3. Position the slide directly on top of the original
  4. Hide it
  5. Save a reference to the slide it was duplicated from

Then you initiate the Sortable method on just the originals. Then when you start dragging a slide:

  1. Hide all the original slides
  2. Reveal the clones
  3. As you drag around, the originals will be re-arranging invisibly
  4. After they do that, animate the clones to those new positions

When the dragging stops:

  1. Make sure all the clones are in the right final position
  2. Swap out the visibility again, revealing the originals

It took a good amount of time to wrap my head around and tinker with to get just right. In my example I use psuedo-elements to number the slides, so there is a bit of code for that as well. Here it is all together.

$(".slide").each(function(i) {
  var item = $(this);
  var item_clone = item.clone();
  item.data("clone", item_clone);
  var position = item.position();
  item_clone
  .css({
    left: position.left,
    top: position.top,
    visibility: "hidden"
  })
    .attr("data-pos", i+1);
  
  $("#cloned-slides").append(item_clone);
});

$(".all-slides").sortable({
  
  axis: "y",
  revert: true,
  scroll: false,
  placeholder: "sortable-placeholder",
  cursor: "move",

  start: function(e, ui) {
    ui.helper.addClass("exclude-me");
    $(".all-slides .slide:not(.exclude-me)")
      .css("visibility", "hidden");
    ui.helper.data("clone").hide();
    $(".cloned-slides .slide").css("visibility", "visible");
  },

  stop: function(e, ui) {
    $(".all-slides .slide.exclude-me").each(function() {
      var item = $(this);
      var clone = item.data("clone");
      var position = item.position();

      clone.css("left", position.left);
      clone.css("top", position.top);
      clone.show();

      item.removeClass("exclude-me");
    });
    
    $(".all-slides .slide").each(function() {
      var item = $(this);
      var clone = item.data("clone");
      
      clone.attr("data-pos", item.index());
    });

    $(".all-slides .slide").css("visibility", "visible");
    $(".cloned-slides .slide").css("visibility", "hidden");
  },

  change: function(e, ui) {
    $(".all-slides .slide:not(.exclude-me)").each(function() {
      var item = $(this);
      var clone = item.data("clone");
      clone.stop(true, false);
      var position = item.position();
      clone.animate({
        left: position.left,
        top: position.top
      }, 200);
    });
  }
  
});

And the demo:

See the Pen Slide Rearranger like Keynote by Chris Coyier (@chriscoyier) on CodePen.

Bonus feature: the slides get a class when being drug which pulsates their size, like in Keynote.

Packery & Dragabilly

David DeSandro has some projects you may have heard of all about rearranging boxes on the web, like Masonry and Isotope. He also has one called Packery which uses an algorithm to pack boxes into spaces. Along with another project of his, Dragabilly, you can create the same effect where dragging an element moves others out of the way. The single-axis nature of our demo is easy work for it.

David forked my demo and made it work with those tools, and it's a good bit less code:

See the Pen Slide Rearranger like Keynote - with Packery and Draggabilly by David DeSandro (@desandro) on CodePen.