Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle cells with rowspan when hiding table rows

I have a table containing cells with rowspan attributes, I would like to:

  1. Whenever a tr is hidden, the table will rearrange itself correctly
  2. Whenever a tr is shown again, it will be restored to original state

So if you have a table like this clicking on X shouldn't destroy the layout. and click a come back button, should restore the original layout.

(try removing all rows from bottom-up, and than restoring them from right-to-left, this is a desired flow)

I had some semi-solutions, but all seem too complicated, and i'm sure there is a nice way to handle this.

like image 944
YardenST Avatar asked Jul 11 '13 22:07

YardenST


People also ask

What is aria rowspan?

The aria-rowspan attribute defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid.

Can we hide a table row in HTML?

A hidden attribute on a <tr> tag hides the table row. Although the table row is not visible, its position on the page is maintained.

Can we use Rowspan and colspan together?

Of course, you can mix colspan and rowspan to get a range of various tables. Example 4-13 demonstrates a mix of column and row spanning.

How do I hide one row in a table?

I add style="display:none;" to my table rows all the time and it effectively hides the entire row. worked for me.


2 Answers

OK I really spent a hell of a long time over this question, so here goes...

For those of you who just want to see the working solution, click here

Update: I've changed the visual columns calculation method to iterate over the table and create a 2-dimensional array, to see the old method which used the jQuery offset() method, click here. The code is shorter, but more time costly.

The problem exists because when we hide a row, whilst we want all the cells to be hidden, we want the pseudo-cells — that is, the cells that appear to be in the following rows due to the cells rowspan attribute — to persist. To get around this, whenever we come across a hidden cell with a rowspan, we try to move it down the the next visible row (decrementing it's rowspan value as we go). With either our original cell or it's clone, we then iterate down the table once more for every row that would contain a pseudo-cell, and if the row is hidden we decrement the rowspan again. (To understand why, look at the working example, and note that when the blue row is hidden, red cell 9's rowspan must be reduced from 2 to 1, else it would push green 9 right).

With that in mind, we must apply the following function whenever rows are shown/hidden:

function calculate_rowspans() {
  // Remove all temporary cells
  $(".tmp").remove();

  // We don't care about the last row
  // If it's hidden, it's cells can't go anywhere else
  $("tr").not(":last").each(function() {
    var $tr = $(this);

    // Iterate over all non-tmp cells with a rowspan    
    $("td[rowspan]:not(.tmp)", $tr).each(function() {
      $td = $(this);
      var $rows_down = $tr;
      var new_rowspan = 1;

      // If the cell is visible then we don't need to create a copy
      if($td.is(":visible")) {
        // Traverse down the table given the rowspan
        for(var i = 0; i < $td.data("rowspan") - 1; i ++) {

          $rows_down = $rows_down.next();
          // If our cell's row is visible then it can have a rowspan
          if($rows_down.is(":visible")) {
            new_rowspan ++;
          }
        }
        // Set our rowspan value
        $td.attr("rowspan", new_rowspan);   
      }
      else {
        // We'll normally create a copy, unless all of the rows
        // that the cell would cover are hidden
        var $copy = false;
        // Iterate down over all rows the cell would normally cover
        for(var i = 0; i < $td.data("rowspan") - 1; i ++) {
          $rows_down = $rows_down.next();
          // We only consider visible rows
          if($rows_down.is(":visible")) {
            // If first visible row, create a copy

            if(!$copy) {
              $copy = $td.clone(true).addClass("tmp");
              // You could do this 1000 better ways, using classes e.g
              $copy.css({
                "background-color": $td.parent().css("background-color")
              });
              // Insert the copy where the original would normally be
              // by positioning it relative to it's columns data value 
              var $before = $("td", $rows_down).filter(function() {
                return $(this).data("column") > $copy.data("column");
              });
              if($before.length) $before.eq(0).before($copy);
              else $(".delete-cell", $rows_down).before($copy);
            }
            // For all other visible rows, increment the rowspan
            else new_rowspan ++;
          }
        }
        // If we made a copy then set the rowspan value
        if(copy) copy.attr("rowspan", new_rowspan);
      }
    });
  });
}

The next, really difficult part of the question is calculating at which index to place the copies of the cells within the row. Note in the example, blue cell 2 has an actual index within its row of 0, i.e. it's the first actual cell within the row, however we can see that visually it lies in column 2 (0-indexed).

I took the approach of calculating this only once, as soon as the document is loaded. I then store this value as a data attribute of the cell, so that I can position a copy of it in the right place (I've had many Eureka moments on this one, and made many pages of notes!). To do this calculation, I ended up constructing a 2-dimensional Array matrix which keeps track of all of the used-visual columns. At the same time, I store the cells original rowspan value, as this will change with hiding/showing rows:

function get_cell_data() {
    var matrix = [];  
    $("tr").each(function(i) {
        var $cells_in_row = $("td", this);
        // If doesn't exist, create array for row
        if(!matrix[i]) matrix[i] = [];
        $cells_in_row.each(function(j) {
            // CALCULATE VISUAL COLUMN
            // Store progress in matrix
            var column = next_column(matrix[i]);
            // Store it in data to use later
            $(this).data("column", column);
            // Consume this space
            matrix[i][column] = "x";
            // If the cell has a rowspan, consume space across
            // Other rows by iterating down
            if($(this).attr("rowspan")) {
                // Store rowspan in data, so it's not lost
                var rowspan = parseInt($(this).attr("rowspan"));
                $(this).data("rowspan", rowspan);
                for(var x = 1; x < rowspan; x++) {
                    // If this row doesn't yet exist, create it
                    if(!matrix[i+x]) matrix[i+x] = [];
                    matrix[i+x][column] = "x";
                }
            }
        });
    });

    // Calculate the next empty column in our array
    // Note that our array will be sparse at times, and
    // so we need to fill the first empty index or push to end
    function next_column(ar) {
        for(var next = 0; next < ar.length; next ++) {
            if(!ar[next]) return next;
        }
        return next;
    }
}

Then simply apply this on page load:

$(document).ready(function() {
    get_cell_data();
});

(Note: whilst the code here is longer than my jQuery .offset() alternative, it's probably quicker to calculate. Please correct me if I'm wrong).

like image 77
Ian Clark Avatar answered Oct 26 '22 19:10

Ian Clark


Working solution - http://codepen.io/jmarroyave/pen/eLkst

This is basically the same solution that i presented before, i just changed how to get the column index to remove the restriction of the jquery.position, and did some refactor to the code.

function layoutInitialize(tableId){
  var layout = String();
  var maxCols, maxRows, pos, i, rowspan, idx, xy;

  maxCols = $(tableId + ' tr').first().children().length;
  maxRows = $(tableId + ' tr').length;

  // Initialize the layout matrix
  for(i = 0; i < (maxCols * maxRows); i++){
    layout += '?';
  }

  // Initialize cell data
  $(tableId + ' td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    rowspan = 1;
    if($(this).attr('rowspan')){
      rowspan = $(this).attr("rowspan");  
      $(this).data("rowspan", rowspan);  
    }

    // Look for the next position available
    idx = layout.indexOf('?');
    pos = {x:idx % maxCols, y:Math.floor(idx / maxCols)}; 
    // store the column index in the cell for future reposition
    $(this).data('column', pos.x);
    for(i = 0; i < rowspan; i++){
      // Mark this position as not available
      xy = (maxCols * pos.y) + pos.x
      layout = layout.substr(0, xy + (i * maxCols)) + 'X' + layout.substr(xy + (i * maxCols)  + 1);
    }
  });   

}

Solution: with jquery.position() - http://codepen.io/jmarroyave/pen/rftdy

This is an alternative solution, it assumes that the first row contains all the information about the number of the table columns and the position of each on.

This aproach has the restriction that the inizialitation code must be call when the table is visible, because it depends on the visible position of the columns.

If this is not an issue, hope it works for you

Initialization

  // Initialize cell data
  $('td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    $(this).data('posx', $(this).position().left);
    if($(this).attr('rowspan')){
      $(this).data("rowspan", $(this).attr("rowspan"));  
    }
  });

UPDATE According to this post ensuring the visibility of the table can be manage with

  $('table').show();
  // Initialize cell data
  $('td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    $(this).data('posx', $(this).position().left);
    if($(this).attr('rowspan')){
      $(this).data("rowspan", $(this).attr("rowspan"));  
    }
  });
  $('table').hide();

As Ian said, the main issue to solve in this problem is to calculate the position of the cells when merging the hidden with the visible rows.

I tried to figure it out how the browser implements that funcionality and how to work with that. Then looking the DOM i searched for something like columnVisiblePosition and i found the position attributes and took that way

 function getColumnVisiblePostion($firstRow, $cell){
  var tdsFirstRow = $firstRow.children();
  for(var i = 0; i < tdsFirstRow.length; i++){
    if($(tdsFirstRow[i]).data('posx') == $cell.data('posx')){
      return i;
    }
  }
}

The js code

$(document).ready(function () {
  add_delete_buttons();

  $(window).on("tr_gone", function (e, tr) {
    add_come_back_button(tr);
  });

  // Initialize cell data
  $('td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    $(this).data('posx', $(this).position().left);
    if($(this).attr('rowspan')){
      $(this).data("rowspan", $(this).attr("rowspan"));  
    }
  });
});

function calculate_max_rowspans() {
  // Remove all temporary cells
  $(".tmp").remove();

  // Get all rows
  var trs = $('tr'), tds, tdsTarget,
      $tr, $trTarget, $td, $trFirst,
      cellPos, cellTargetPos, i;

  // Get the first row, this is the layout reference
  $trFirst = $('tr').first();

  // Iterate through all rows
  for(var rowIdx = 0; rowIdx < trs.length; rowIdx++){
    $tr = $(trs[rowIdx]);
    $trTarget = $(trs[rowIdx+1]);
    tds = $tr.children();

    // For each cell in row
    for(cellIdx = 0; cellIdx < tds.length; cellIdx++){
      $td = $(tds[cellIdx]);
      // Find which one has a rowspan
      if($td.data('rowspan')){
        var rowspan = Number($td.data('rowspan'));

        // Evaluate how the rowspan should be display in the current state
        // verify if the cell with rowspan has some hidden rows
        for(i = rowIdx; i < (rowIdx + Number($td.data('rowspan'))); i++){
          if(!$(trs[i]).is(':visible')){
            rowspan--;
          }
        }

        $td.attr('rowspan', rowspan);

        // if the cell doesn't have rows hidden within, evaluate the next cell
        if(rowspan == $td.data('rowspan')) continue;

        // If this row is hidden copy the values to the next row
        if(!$tr.is(':visible') && rowspan > 0) {
          $clone = $td.clone();
          // right now, the script doesn't care about copying data, 
          // but here is the place to implement it
          $clone.data('rowspan', $td.data('rowspan') - 1);
          $clone.data('posx', $td.data('posx'));
          $clone.attr('rowspan',  rowspan);
          $clone.addClass('tmp');

          // Insert the temp node in the correct position
          // Get the current cell position
          cellPos = getColumnVisiblePostion($trFirst, $td);

          // if  is the last just append it
          if(cellPos == $trFirst.children().length - 1){
            $trTarget.append($clone);
          }
          // Otherwise, insert it before its closer sibling
          else {
            tdsTarget = $trTarget.children();
            for(i = 0; i < tdsTarget.length; i++){
              cellTargetPos = getColumnVisiblePostion($trFirst, $(tdsTarget[i]));
              if(cellPos < cellTargetPos){
                $(tdsTarget[i]).before($clone);  
                break;
              }
            }                
          }          
        }
      } 
    }

    // remove tmp nodes from the previous row 
    if(rowIdx > 0){
      $tr = $(trs[rowIdx-1]);
      if(!$tr.is(':visible')){
        $tr.children(".tmp").remove();  
      }

    } 
  }
}

// this function calculates the position of a column 
// based on the visible position
function getColumnVisiblePostion($firstRow, $cell){
  var tdsFirstRow = $firstRow.children();
  for(var i = 0; i < tdsFirstRow.length; i++){
    if($(tdsFirstRow[i]).data('posx') == $cell.data('posx')){
      return i;
    }
  }
}

function add_delete_buttons() {
  var $all_rows = $("tr");
  $all_rows.each(function () {
    // TR to remove
    var $tr = $(this);
    var delete_btn = $("<button>").text("x");
    delete_btn.on("click", function () {
      $tr.hide();
      calculate_max_rowspans();
      $(window).trigger("tr_gone", $tr);
    });
    var delete_cell = $("<td>");
    delete_cell.append(delete_btn);
    $(this).append(delete_cell);
  });
}

function add_come_back_button(tr) {
  var $tr = $(tr);
  var come_back_btn = $("<button>").text("come back " + $tr.attr("color_class"));
  come_back_btn.css({"background": $(tr).css("background")});
  come_back_btn.on("click", function () {
    $tr.show();
    come_back_btn.remove();
    calculate_max_rowspans();
  });
  $("table").before(come_back_btn);
}

if you have any questions or comments let me know.

like image 33
jmarroyave Avatar answered Oct 26 '22 17:10

jmarroyave