7

I have a directive which renders a HTML table where each td element has an id

What I want to accomplish is to use the mousedown.dragselect/mouseup.dragselect to determine which elements have been selected, and then highlight those selected elements. What I have so far is something like this:

var $ele = $(this);
scope.bindMultipleSelection = function() {
  element.bind('mousedown.dragselect', function() {
    $document.bind('mousemove.dragselect', scope.mousemove);
    $document.bind('mouseup.dragselect', scope.mouseup);
  });
};

scope.bindMultipleSelection();

scope.mousemove = function(e) {
  scope.selectElement($(this));
};

scope.mouseup = function(e) {
};

scope.selectElement = function($ele) {
  if (!$ele.hasClass('eng-selected-item'))
    $ele.addClass('eng-selected-item'); //apply selection or de-selection to current element
};

How can I get every td element selected by mousedown.dragselect, and be able to get their ids and then highlight them?

j.wittwer
  • 9,497
  • 3
  • 30
  • 32
user1489941
  • 281
  • 4
  • 9

1 Answers1

12

I suspect using anything relating to dragging won't give you what you want. Dragging is actually used when moving elements about (e.g. dragging files in My Computer / Finder), when what you're after is multiple selection.

So there a number of things the directive needs:

  • Listen to mousedown, mouseenter and mouseup, events.

    • mousedown should listen on the cells of the table, and set a "dragging" mode.
    • mouseenter should listen on the cells as well, and if the directive is in dragging mode, select the "appropriate cells"
    • mouseup should disable dragging mode, and actually be on the whole body, in case the mouse is lifted up while the cursor is not over the table.
  • jQuery delegation is useful here, as it can nicely delegate the above events to the table, so the code is much more friendly to cells that are added after this directive is initialised. (I wouldn't include or use jQuery in an Angular project unless you have a clear reason like this).

  • Although you've not mentioned it, the "appropriate cells" I suspect all the cells "between" where the mouse was clicked, and the current cell, chosen in a rectangle, and not just the cells that have been entered while the mouse was held down. To find these, cellIndex and rowIndex can be used, together with filtering all the cells from the table.

  • All the listeners should be wrapped $scope.$apply to make sure Angular runs a digest cycle after they fire.

  • For the directive to communicate the ids of the selected elements to the surrounding scope, the directive can use bi-directional binding using the scope property, and the = symbol, as explained in the Angular docs

Putting all this together gives:

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));
    }
  }
});

Another thing that will make the experience a bit nicer, is to set the cursor to a pointer, and disable text selection

[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;
}

You can also see this in action in this working demo

Community
  • 1
  • 1
Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
  • Hi Michal Charemza, Thank you for your response, your solution work for me. – user1489941 Apr 22 '14 at 20:51
  • I modified a little bit to be used by other directives. I used $rootScope.$broadcast('DRAG_SELECTED_UPDATE',$scope.dragSelectIds); – user1489941 Apr 22 '14 at 20:56
  • Using `$rootScope.$broadcast` might end up being limiting in the long run, as it will make it difficult to have more than one of such tables in the same app at the same time. – Michal Charemza Apr 22 '14 at 21:15
  • What do you suggest to do instead of using $rootScope.$broadcast? – user1489941 Apr 23 '14 at 17:07
  • Ok. I used http://thesmithfam.org/blog/2012/12/17/communicating-between-directives-in-angularjs/ instead of using $rootScope.$broadcast – user1489941 Apr 24 '14 at 00:18
  • I believe `$scope.dragSelectIds = [];` should be changed to `$scope.dragSelectIds.length = 0;`, otherwise the original scope variable will not be updated. – Uri Jun 19 '14 at 13:21
  • Is there any way to keep a square/rectangular selection if there are colspans/rowspans (of greater than 1) on some of the cells? – Kyle Krzeski Jun 12 '17 at 12:52