在隐藏表行时处理具有rowspan的单元格

时间:2013-07-11 22:06:25

标签: javascript jquery html algorithm

我有table containing cells with rowspan attributes,我想:

  1. 每当隐藏tr时,表格将自动重新排列
  2. 每当再次显示tr时,它将恢复到原始状态
  3. 因此,如果X点击come back,则不应销毁布局。 并单击{{1}}按钮,应恢复原始布局。

    (尝试从下到上删除所有行,然后从右到左恢复它们,这是一个理想的流程)

    我有一些半解决方案,但似乎都太复杂了,我确信有一个很好的方法可以解决这个问题。

3 个答案:

答案 0 :(得分:23)

好的,我真的花了很长时间来讨论这个问题,所以这里......

对于那些只想查看有效工作解决方案的人,click here

更新:我已经更改了可视列计算方法来迭代表并创建一个二维数组,以查看使用jQuery offset()方法的旧方法, click here。代码更短,但时间更长。

存在问题是因为当我们隐藏一行时,我们想要隐藏所有单元格,我们想要伪单元格 - 即看起来位于以下行中的单元格由于单元格rowspan属性 - 持续存在。为了解决这个问题,每当我们遇到一个带有rowspan的隐藏单元格时,我们会尝试将其向下移动到下一个可见行(随着时间的推移递减rowspan值)。对于我们的原始单元格或它的克隆,我们再次对包含伪单元格的每一行向下迭代表格,如果该行被隐藏,我们再次递减rowspan。 (要了解原因,请查看工作示例,并注意隐藏蓝色行时,红色单元格9的行数必须从2减少到1,否则它会将绿色9向右推)。

考虑到这一点,我们必须在显示/隐藏行时应用以下函数:

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

接下来,真正问题的难点在于计算将哪些单元格的副本放在行中的索引。注意在示例中,蓝色单元格2在其行0中具有实际索引,即它是该行中的第一个实际单元格,但是我们可以看到它在视觉上位于第2列(0索引) )。

加载文档后,我只采用了计算一次的方法。然后我将这个值存储为单元格的数据属性,这样我就可以将它的副本放在正确的位置(我在这个上面有很多Eureka时刻,并制作了许多页面的注释!)。为了进行这个计算,我最终构建了一个二维数组matrix,它跟踪所有使用过的可视列。同时,我存储单元格原始rowspan值,因为这将随着隐藏/显示行而改变:

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

然后在页面加载上简单地应用它:

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

(注意:虽然这里的代码比我的jQuery .offset()替代,但它的计算速度可能更快。如果我错了,请纠正我。)

答案 1 :(得分:1)

工作解决方案 - http://codepen.io/jmarroyave/pen/eLkst

这与我之前提出的解决方案基本相同,我只是改变了如何获取列索引以消除jquery.position的限制,并对代码进行了一些重构。

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

}

解决方案:使用jquery.position() - http://codepen.io/jmarroyave/pen/rftdy

这是一种替代解决方案,它假定第一行包含有关表列数和每个列位置的所有信息。

这个aproach 的限制是当表格可见时必须调用inizialitation代码,因为它取决于列的可见位置。

如果这不是问题,希望它适合你

<强>初始化

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

<强>更新 根据此post确保可以使用

管理表的可见性
  $('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();

正如伊恩所说,在这个问题中要解决的主要问题是在将隐藏行与可见行合并时计算单元格的位置。

我试图弄清楚浏览器如何实现该功能以及如何使用它。然后查看DOM我搜索了像columnVisiblePosition这样的东西,我找到了位置属性并采取了这种方式

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

js代码

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

如果您有任何问题或意见,请告诉我。

答案 2 :(得分:0)

我假设您希望在隐藏行时向上移动行,但不希望单元格向左移动。

这是我得到的http://codepen.io/anon/pen/prDcK

我添加了两条css规则:

#come_back_container{height: 30px;}
td[rowspan='0']{background-color: white;}

这是我使用的html:

<div id="come_back_container"></div>
<table id="dynamic_table" cellpadding=7></table>
<table id="dynamic_table2" cellpadding=7>
  <tr style="background-color: red">
    <td rowspan="5">a</td>
    <td rowspan="1">b</td>
    <td rowspan="5">c</td>
    <td rowspan="1">d</td>
    <td rowspan="2">e</td>
  </tr>
  <tr style="background-color: grey">
    <td rowspan="0">f</td>
    <td rowspan="1">g</td>
    <td rowspan="0">h</td>
    <td rowspan="1">i</td>
    <td rowspan="0">j</td>
  </tr>
  <tr style="background-color: blue">
    <td rowspan="0">k</td>
    <td rowspan="1">l</td>
    <td rowspan="0">m</td>
    <td rowspan="1">n</td>
    <td rowspan="1">o</td>
  </tr>
  <tr style="background-color: yellow">
    <td rowspan="0">p</td>
    <td rowspan="1">q</td>
    <td rowspan="0">r</td>
    <td rowspan="1">s</td>
    <td rowspan="2">t</td>
  </tr>
  <tr style="background-color: green">
    <td rowspan="0">u</td>
    <td rowspan="1">v</td>
    <td rowspan="0">w</td>
    <td rowspan="1">x</td>
    <td rowspan="0">y</td>
  </tr>
</table>

第一条规则就是将表格的上边缘保持在同一个位置。第二个规则是通过与背景混合使单元格显示为空白,因此相应地更改。

最后这里是js:

$(function () {
  //firstTable()

  var myTb2 = new dynamicTable();
  myTb2.createFromElement( $("#dynamic_table2") );
  myTb2.drawTable()

  $(window).on("tr_hide", function (e,data){
    var tbl = data.ctx,
        rowIndex = data.idx;
    tbl.hideRow.call(tbl, rowIndex);
  })
  $(window).on("tr_show", function (e,data){
    var tbl = data.ctx,
        rowIndex = data.idx;
    tbl.showRow.call(tbl, rowIndex);
  })
})

function dynamicTableItem(){
  this.height = null;
  this.content = null;
}

function dynamicTableRow(){
  this.color = null;
  this.items = []
  this.show = true

  this.setNumColumns = function(numCols){
    for(var i=0;i<numCols;i++){
      var item = new dynamicTableItem(); 
      item.height = 0;
      this.items.push(item)
    }
  }

  this.addItem = function(index, height, content){
    var item = new dynamicTableItem();
    item.height = height;
    item.content = content;
    if(index>=this.items.length){ console.error("index out of range",index); }
    this.items[index] = item;
  }
}

function dynamicTable(){
  this.element = null;
  this.numCols = null;
  this.rows = []

  this.addRow = function(color){
    var row = new dynamicTableRow();
    row.color = color;
    row.setNumColumns(this.numCols)
    var length = this.rows.push( row )
    return this.rows[length-1]
  }
  this.drawTable = function(){
    this.element.empty()

    var cols = [],
        rowElements = [];
    for(var i=0;i<this.numCols;i++){
      cols.push( [] )
    }

    for(var r=0; r<this.rows.length; r++){
      var row = this.rows[r]
      if(row.show){
        var $tr = $("<tr>"),
            delete_cell = $("<td>"),
            delete_btn = $("<button>").text("x")
        var data = {ctx: this, idx: r};
        delete_btn.on("click", data, function(e){
          $(window).trigger("tr_hide", e.data);
        })
        delete_cell.addClass("deleteCell");
        $tr.css( {"background": row.color} );

        delete_cell.append(delete_btn);
        $tr.append(delete_cell);
        this.element.append($tr);
        rowElements.push( $tr );

        for(var i=0; i<row.items.length; i++){
          cols[i].push( row.items[i] );
        }
      }
    }

    for(var c=0; c<cols.length; c++){
      var cellsFilled = 0;
      for(var r=0; r<cols[c].length; r++){
        var item = cols[c][r]
        var size = item.height;
        if(r>=cellsFilled){
          cellsFilled += (size>0 ? size : 1);
          var el = $("<td>").attr("rowspan",size);
          el.append(item.content);
          rowElements[r].children().last().before(el);
        }
      }
    }
  }

  this.hideRow = function(rowIndex){
    var row = this.rows[rowIndex]
    row.show = false; 

    var come_back_btn = $("<button>").text("come back");
    come_back_btn.css( {"background": row.color} );
    var data = {ctx:this, idx:rowIndex};
    come_back_btn.on("click", data, function(e){
      $(window).trigger("tr_show", e.data);
      $(this).remove();
    });
    $("#come_back_container").append(come_back_btn);

    this.drawTable();
  }

  this.showRow = function(rowIndex){
    this.rows[rowIndex].show = true;
    this.drawTable();
  }

  this.createFromElement = function(tbl){
    this.element = tbl;
    var tblBody = tbl.children().filter("tbody")
    var rows = tblBody.children().filter("tr")
    this.numCols = rows.length

    for(var r=0;r<rows.length;r++){
      var row = this.addRow( $(rows[r]).css("background-color") );
      var items = $(rows[r]).children().filter("td");

      for(var i=0;i<items.length;i++){
        var item = $(items[i]);
        var height = parseInt(item.attr("rowspan"));
        var contents = item.contents();
        row.addItem(i,height,contents);
      }
    }
    //console.log(this); 
  }

}

function firstTable(){
  var myTable = new dynamicTable();
  myTable.element = $("#dynamic_table");
  myTable.numCols = 5

  var red = myTable.addRow("red"); 
  red.addItem(0,5);
  red.addItem(1,1);
  red.addItem(2,5);
  red.addItem(3,1);
  red.addItem(4,2);

  var white = myTable.addRow("grey");
  //white.addItem(0,0);
  white.addItem(1,1);
  //white.addItem(2,0);
  white.addItem(3,1);
  //white.addItem(4,0);

  var blue = myTable.addRow("blue");
  //blue.addItem(0,3);  //try uncommenting this and removing red
  blue.addItem(1,1);
  //blue.addItem(2,0);
  blue.addItem(3,1);
  blue.addItem(4,1);

  var yellow = myTable.addRow("yellow");
  //yellow.addItem(0,0);
  yellow.addItem(1,1);
  //yellow.addItem(2,0);
  yellow.addItem(3,1);
  yellow.addItem(4,2);

  var green = myTable.addRow("green");
  //green.addItem(0,0);
  green.addItem(1,1);
  //green.addItem(2,0);
  green.addItem(3,1);
  //green.addItem(4,0);

  myTable.drawTable();
}

我尝试使用明确的变量和方法名称,但如果你有任何任务,请问。

PS-我知道目前没有简单的方法向细胞添加内容,但你只要求消失的行。