Matthew Schlachter
Matthew Schlachter

Reputation: 3350

Exclude element from JQuery-UI containment

I am trying to allow an element to be dragable/resizable inside of another element but not in the area of another child of that element.

Given the following table row: table row with blue area spanning three columns and the fifth column greyed out

I want to be able to drag and resize the blue area anywhere within the table row, but not where the grey table cell is (i.e. it would move/resize freely within the first four cells but be stopped when it reaches the last one).

I can constrain it to the table row using the containment: '.middle tr' or containment: $el.closest('tr'), (where $el is a selector for the blue element) options in JQuery.draggable and JQuery.resizable, but I haven't been able to find a way that works for excluding the last column from the containment.

How do you exclude an element from the containment area?

Example with code:

$(function() {
  $("td").droppable({
    drop: function(event, ui) {
      var draggable = ui.draggable;

      var newLeft = draggable.offset().left - draggable.closest(".middle table").offset().left;
      var childNum = Math.round((newLeft + 100) / 100);

      var newContainer = $(this).closest('tr').find('td:nth-of-type(' + childNum + ')');

      draggable.css({
        top: '',
        left: ''
      });

      newContainer.append(draggable);
    },
  });
  $(".planning-spot").resizable({
    grid: [100, 10000000],
    handles: "e",
    containment: ".middle tr"
  }).each(function() {
    var $el = $(this);
    $el.draggable({
      containment: $el.closest('tr'),
      axis: 'x',
    });
  });
});
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

table {
  border-collapse: collapse;
  overflow: hidden;
  table-layout: fixed;
  width: 500px;
  margin: 20px;
}

tr {
  position: relative;
  background: #FFF;
}

td, th {
  position: relative;
  padding: 0;
  width: 100px;
  height: 30px;
  border: 1px solid #000;
}

.planning-spot {
  position: absolute;
  top: 0;
  left: 0;
  height: 20px;
  width: 90px;
  border-radius: 10px;
  margin: 5px;
  text-align: center;
  z-index: 1;
  cursor: pointer;
  color: #FFF;
  width: 290px;
  background: #3CF;
}

.no-planning {
  background: #CCC;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<link href="https://code.jquery.com/ui/1.12.1/jquery-ui.min.css" rel="stylesheet"/>
<div class="wrapper">
    <div class="middle">
      <table>
        <tbody>
          <tr>
            <td>
              <div></div>
              <div class="planning-spot">18 / 20</div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td class="no-planning">
              <div></div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
</div>

http://jsfiddle.net/xpvt214o/668937/

I'm aware that I can check the location during the drop and stop functions of droppable and resizable respectively and reverse the action if needed (which is my current solution), but I'm looking for a solution to prevent it being moved into an "unwanted" area in the first place.

Upvotes: 3

Views: 490

Answers (2)

scraaappy
scraaappy

Reputation: 2886

You can determine the draggable area with getBoundingClientRect() and assign containment to an array wich take the width of each resizeable elements

$(function() {

  $("td").droppable({
    drop: function(event, ui) {
      var draggable = ui.draggable;

      var newLeft = draggable.offset().left - draggable.closest(".middle table").offset().left;
      var childNum = Math.round((newLeft + 100) / 100);

      var newContainer = $(this).closest('tr').find('td:nth-of-type(' + childNum + ')');

      draggable.css({
        top: '',
        left: ''
      });

      newContainer.append(draggable);
    },
  });
  $(".planning-spot").resizable({
    grid: [100, 10000000],
    handles: "e",
    containment: ".middle tr"
  }).each(function() {
    var $el = $(this);
    var bBoxTr = $el.closest('tr')[0].getBoundingClientRect();
    var bBoxTd = $el.closest('tr').children(".no-planning")[0].getBoundingClientRect();
    var x1 = bBoxTr.x, y1 = bBoxTr.y;
    var x2 = bBoxTr.right-bBoxTd.width, y2 = bBoxTr.bottom;
    $el.draggable({
//      containment: $el.closest('tr'),
      containment: [x1,y1,x2-$el.width()-10,y2],
      axis: 'x',
    });
  });
});
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

table {
  border-collapse: no-collapse;
  overflow: hidden;
  table-layout: fixed;
  width: 500px;
  margin: 20px;
}

tr {
  position: relative;
  background: #FFF;
}

td, th {
  position: relative;
  padding: 0;
  width: 100px;
  height: 30px;
  border: 1px solid #000;
}

.planning-spot {
  position: absolute;
  top: 0;
  left: 0;
  height: 20px;
  width: 90px;
  border-radius: 10px;
  margin: 5px;
  text-align: center;
  z-index: 1;
  cursor: pointer;
  color: #FFF;
  width: 290px;
  background: #3CF;
}

.no-planning {
  background: #CCC;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<link href="https://code.jquery.com/ui/1.12.1/jquery-ui.min.css" rel="stylesheet"/>
<div class="wrapper">
    <div class="middle">
      <table>
        <tbody>
          <tr id="test">
            <td>
              <div></div>
              <div class="planning-spot">18 / 20</div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td class="no-planning">
              <div></div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
</div>

Edit:

Since getBoundingClientRect(); is not supported by all browsers, better go with jquery features, position(), height() and width();.

See the revised snippet below, the function getDragArea returns an array with the coordinates of the drag area. I add a listener (mouseover) which permit to reset drag area when needed. The table is wrapped in a scrolling div to test behavior.

$(function() {

  $("td").droppable({
    drop: function(event, ui) {
      var draggable = ui.draggable;

      var newLeft = draggable.offset().left - draggable.closest(".middle table").offset().left;
      var childNum = Math.round((newLeft + 100) / 100);

      var newContainer = $(this).closest('tr').find('td:nth-of-type(' + childNum + ')');

      draggable.css({
        top: '',
        left: ''
      });

      newContainer.append(draggable);
    },
  });
  $(".planning-spot").resizable({
    grid: [100, 10000000],
    handles: "e",
    containment: ".middle tr"
  }).each(function() {
    $(this).on('mouseover',function(){
      $(this).draggable({
      containment: getDragArea($(this)),
      axis: 'x',
    });
	})
  });
  
 function getDragArea($el){
    var $tr = $el.closest('tr');
  	var $tds = $el.closest('tr').children(".no-planning");
	  var width = $el.closest('tr').width()-$tds.width()*$tds.length;
	  var height = $el.closest('tr').height();
    var position = $el.closest('tr').position();
    var x1 = position.left, y1 = position.top;
    var x2 = x1+width, y2 = y1+height;
    return [x1,y1,x2-$el.width()-10,y2]
   }
});
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

table {
  border-collapse: no-collapse;
  overflow: hidden;
  table-layout: fixed;
  width: 500px;
  margin: 20px;
}

tr {
  position: relative;
  background: #FFF;
}

td, th {
  position: relative;
  padding: 0;
  width: 100px;
  height: 30px;
  border: 1px solid #000;
}

.planning-spot {
  position: absolute;
  top: 0;
  left: 0;
  height: 20px;
  width: 90px;
  border-radius: 10px;
  margin: 5px;
  text-align: center;
  z-index: 1;
  cursor: pointer;
  color: #FFF;
  width: 290px;
  background: #3CF;
}

.no-planning {
  background: #CCC;
}

.wrapper{
  overflow:auto;
  width:500px;
  height:120px;
  border: solid 1px red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<link href="https://code.jquery.com/ui/1.12.1/jquery-ui.min.css" rel="stylesheet"/>
<div class="wrapper">
    <div class="middle">
      <table>
        <tbody>
          <tr id="test">
            <td>
              <div></div>
              <div class="planning-spot">18 / 20</div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td class="no-planning">
              <div></div>
            </td>
            <td class="no-planning">
              <div></div>
            </td>
          </tr>
        </tbody>
      </table>
      <br>
      <br>
      <br>
      <br>
      <br>
      <br>
      <br>
      <br>
    </div>
</div>

Upvotes: 2

Dipen Shah
Dipen Shah

Reputation: 26085

I did the trick by checking if drag event will cause collision with non dragable item.

Checkout code below:

$(function() {
	var nonDragableItems = $(".no-planning");
  $("td").droppable({
    drop: function(event, ui) {
      var draggable = ui.draggable;

      var newLeft = draggable.offset().left - draggable.closest(".middle table").offset().left;
      var childNum = Math.round((newLeft + 100) / 100);

      var newContainer = $(this).closest('tr').find('td:nth-of-type(' + childNum + ')');

      draggable.css({
        top: '',
        left: ''
      });

      newContainer.append(draggable);
    },
  });
  
  function onDrag(e, ui){
    var dragElemRect = ui.helper[0].getBoundingClientRect();
    for(var i=0;i<nonDragableItems.length;i++){
      var elemRect = nonDragableItems[i].getBoundingClientRect();
      if(areColliding(dragElemRect, elemRect))
        return false;
    }
  }

  function areColliding(rect1, rect2){
    if(rect1.bottom <= rect2.top ||
       rect1.top >= rect2.bottom ||
       rect1.left >= rect2.right ||
       rect1.right <= rect2.left)
          return false;
        return true;
  }
  
  $(".planning-spot").resizable({
    grid: [100, 10000000],
    handles: "e",
    containment: ".middle tr"
  }).each(function() {
    var $el = $(this);
    $el.draggable({
      containment: $el.closest('tr'),
      axis: 'x',
      drag: onDrag
    });
  });
});
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

table {
  border-collapse: collapse;
  overflow: hidden;
  table-layout: fixed;
  width: 500px;
  margin: 20px;
}

tr {
  position: relative;
  background: #FFF;
}

td, th {
  position: relative;
  padding: 0;
  width: 100px;
  height: 30px;
  border: 1px solid #000;
}

.planning-spot {
  position: absolute;
  top: 0;
  left: 0;
  height: 20px;
  width: 90px;
  border-radius: 10px;
  margin: 5px;
  text-align: center;
  z-index: 1;
  cursor: pointer;
  color: #FFF;
  width: 290px;
  background: #3CF;
}

.no-planning {
  background: #CCC;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet"/>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

<div class="wrapper">
    <div class="middle">
      <table>
        <tbody>
          <tr>
            <td>
              <div></div>
              <div class="planning-spot">18 / 20</div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td>
              <div></div>
            </td>
            <td class="no-planning">
              <div></div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
</div>

Upvotes: 1

Related Questions