javascriptjqueryjquery-uijquery-ui-draggablejquery-ui-resizable

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.


Solution

  • 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>