1

I'm working on THIS implementation for selecting cells in table, however in my case, cells can have a colspan or rowspan so the selection doesn't restrict to a square/rectangular selection (try selecting "1-3" and "2-3" which should also automatically select "1-4"). It's similar to THIS question, but I haven't been able to get anything to work. Do you know how this would be implemented?

Link: Working Code

HTML

<table drag-select drag-select-ids="ids">
      <tr>
        <td id="td-1-1">1-1</td>
        <td id="td-1-2">1-2</td>
        <td id="td-1-3">1-3</td>
        <td id="td-1-4">1-4</td>
      </tr>
      <tr>
        <td id="td-2-1">2-1</td>
        <td id="td-2-2">2-2</td>
        <td id="td-2-3" colspan="2">2-3</td>
      </tr>
      <tr>
        <td id="td-3-1">3-1</td>
        <td id="td-3-2">3-2</td>
        <td id="td-3-3">3-3</td>
        <td id="td-3-4">3-4</td>
      </tr>
      <tr>
        <td id="td-4-1">4-1</td>
        <td id="td-4-2">4-2</td>
        <td id="td-4-3">4-3</td>
        <td id="td-4-4">4-4</td>
      </tr>
    </table>

JavaScript

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.ids = [];
});

app.directive('dragSelect', function($window, $document) {
  return {
    scope: {
      dragSelectIds: '='
    },
    controller: function($scope, $element) {
      var cls = 'eng-selected-item';
      var startCell = null;
      var dragging = false;

      function mouseUp(el) {
        dragging = false;
      }

      function mouseDown(el) {
        dragging = true;
        setStartCell(el);
        setEndCell(el);
      }

      function mouseEnter(el) {
        if (!dragging) return;
        setEndCell(el);
      }

      function setStartCell(el) {
        startCell = el;
      }

      function setEndCell(el) {
        $scope.dragSelectIds = [];
        $element.find('td').removeClass(cls);
        cellsBetween(startCell, el).each(function() {
          var el = angular.element(this);
          el.addClass(cls);
          $scope.dragSelectIds.push(el.attr('id'));
        });
      }

      function cellsBetween(start, end) {
        var coordsStart = getCoords(start);
        var coordsEnd = getCoords(end);
        var topLeft = {
          column: $window.Math.min(coordsStart.column, coordsEnd.column),
          row: $window.Math.min(coordsStart.row, coordsEnd.row),
        };
        var bottomRight = {
          column: $window.Math.max(coordsStart.column, coordsEnd.column),
          row: $window.Math.max(coordsStart.row, coordsEnd.row),
        };
        return $element.find('td').filter(function() {
          var el = angular.element(this);
          var coords = getCoords(el);
          return coords.column >= topLeft.column
              && coords.column <= bottomRight.column
              && coords.row >= topLeft.row
              && coords.row <= bottomRight.row;
        });
      }

      function getCoords(cell) {
        var row = cell.parents('row');
        return {
          column: cell[0].cellIndex, 
          row: cell.parent()[0].rowIndex
        };
      }

      function wrap(fn) {
        return function() {
          var el = angular.element(this);
          $scope.$apply(function() {
            fn(el);
          });
        }
      }

      $element.delegate('td', 'mousedown', wrap(mouseDown));
      $element.delegate('td', 'mouseenter', wrap(mouseEnter));
      $document.delegate('body', 'mouseup', wrap(mouseUp));
    }
  }
});

CSS

[drag-select] {
  cursor: pointer;
 -webkit-touch-callout: none;
 -webkit-user-select: none;
 -khtml-user-select: none;
 -moz-user-select: none;
 -ms-user-select: none;
 user-select: none;
}

[drag-select] .eng-selected-item {
  background: blue;
  color: white;
}

td {
  padding: 10px;
  border: 1px solid gray;
}
Kyle Krzeski
  • 6,183
  • 6
  • 41
  • 52
  • Should selecting 1-1 as top left and 2-3 as bottom right always also select 1-4? How would you then select 1-1,1-2,1-3,2-1,2-2,2-3? Maybe something like this would be a better fit for the functionality you are looking for: http://nightlycoding.com/index.php/2014/02/click-and-drag-multi-selection-rectangle-with-javascript/ – phil Jun 12 '17 at 13:30
  • Yes. It's supposed to essentially mimic excel's functionality. so the selection should always be a square/rectangle. – Kyle Krzeski Jun 12 '17 at 13:36

1 Answers1

3

I used the x and y coordinates of the start and end cell, then calculate every cell that is inside the corresponding rectangle (even partially). Then get the bounding rectangle for these cells and repeat the process until the selection is not expanding anymore.

EDIT: function rectangleSelect is (almost completely) from : Get DOM elements inside a rectangle area of a page

EDIT 2: Now supporting rowspan and other edgecases like selecting [3-2,2-3]: enter image description here

http://plnkr.co/edit/8wZvcU1SgmieStsqg3lD?p=preview

HTML:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="jquery" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
    <script data-require="angular.js@1.2.x" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js" data-semver="1.2.16"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <table drag-select drag-select-ids="ids">
      <tr>
        <td id="td-1-1">1-1</td>
        <td id="td-1-2">1-2</td>
        <td id="td-1-3">1-3</td>
        <td id="td-1-4">1-4</td>
      </tr>
      <tr>
        <td id="td-2-1" colspan=2>2-1</td>
        <td id="td-2-3" rowspan="2">2-3</td
        ><td id="td-2-4">2-2</td>
      </tr>
      <tr>
        <td id="td-3-1">3-1</td>
        <td id="td-3-2">3-2</td>
        <td id="td-3-4">3-4</td>
      </tr>
      <tr>
        <td id="td-4-1">4-1</td>
        <td id="td-4-2">4-2</td>
        <td id="td-4-3">4-3</td>
        <td id="td-4-4">4-4</td>
      </tr>
    </table>
    <p>Selected IDs: {{ids | json}}</p>
  </body>

</html>

CSS: unchanged

JS:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.ids = [];
});

app.directive('dragSelect', function($window, $document) {
  return {
    scope: {
      dragSelectIds: '='
    },
    controller: function($scope, $element) {
      var cls = 'eng-selected-item';
      var startCell = null;
      var dragging = false;

      function mouseUp(el) {
        dragging = false;
      }

      function mouseDown(el) {
        dragging = true;
        setStartCell(el);
        setEndCell(el);
      }

      function mouseEnter(el) {
        if (!dragging) return;
        setEndCell(el);
      }

      function setStartCell(el) {
        startCell = el;
      }

      function setEndCell(el) {
        $scope.dragSelectIds = [];
        $element.find('td').removeClass(cls);
        $(cellsBetween(startCell, el)).each(function() {
          var el = angular.element(this);
          el.addClass(cls);
          $scope.dragSelectIds.push(el.attr('id'));
        });
      }

      function isPointBetween(point,x1,x2){
        return (point >=x1 && point <=x2) ||(point <=x1 && point>=x2);
      }
      function rectangleSelect(selector, bounds) {
    var elements = [];
    jQuery(selector).each(function() {
        var $this = jQuery(this);
        var offset = $this.offset();
        var x = offset.left;
        var y = offset.top;
        var w = $this.outerWidth();
        var h = $this.outerHeight();
        if ((isPointBetween(x,bounds.minX,bounds.maxX) && isPointBetween(y,bounds.minY,bounds.maxY))||
            (isPointBetween(x+w,bounds.minX,bounds.maxX) && isPointBetween(y+h,bounds.minY,bounds.maxY))
            ) {
            elements.push($this.get(0));
        }
    });
    return elements;
}
      function getBoundsForElements(elements){
        var x1= elements.reduce(function(currMinX,element){
          var elementLeft = $(element).offset().left;
          return currMinX && currMinX<elementLeft ? currMinX : elementLeft;
        },undefined);
        var x2= elements.reduce(function(currMaxX,element){
          var elementRight = $(element).offset().left+$(element).outerWidth();
          return currMaxX && currMaxX>elementRight ? currMaxX : elementRight;
        },undefined);
        var y1= elements.reduce(function(currMinY,element){
          var elementTop = $(element).offset().top;
          return currMinY && currMinY<elementTop ? currMinY : elementTop;
        },undefined);
        var y2= elements.reduce(function(currMaxY,element){
          var elementBottom = $(element).offset().top+$(element).outerHeight();
          return currMaxY && currMaxY>elementBottom ? currMaxY : elementBottom;
        },undefined);
        return {
          minX: x1,
          maxX: x2,
          minY: y1,
          maxY: y2
        };

      }


      function cellsBetween(start, end) {
        var bounds,elementsInside;
        elementsInside = [start,end];
        do{
          bounds = getBoundsForElements(elementsInside);
          var elementsInsideAfterExpansion = rectangleSelect("td",bounds);
          if(elementsInside.length==elementsInsideAfterExpansion.length)
            return elementsInside;
          else
            elementsInside=elementsInsideAfterExpansion;
        }while(true)

      }


      function wrap(fn) {
        return function() {
          var el = angular.element(this);
          $scope.$apply(function() {
            fn(el);
          });
        }
      }

      $element.delegate('td', 'mousedown', wrap(mouseDown));
      $element.delegate('td', 'mouseenter', wrap(mouseEnter));
      $document.delegate('body', 'mouseup', wrap(mouseUp));
    }
  }
});
phil
  • 420
  • 4
  • 14
  • Thanks! Though what about the case where there is a rowspan? http://plnkr.co/edit/RIlCH4G1EHQPFDBD2xSV?p=preview (try dragging from 3-2 to 3-4) – Kyle Krzeski Jun 14 '17 at 13:48
  • Can someone add support for multiple select? Everytime I do a new selection, old one dissappears? – jansv Nov 12 '18 at 01:28
  • It has error when all cells of one row merged, and trying to select vertical one column, acroll the one row. (Like the cross) – Junho Park Apr 26 '19 at 12:08